---
title: "Build a multi-tenant app"
description: "Give every customer a tenant, issue API keys, gate features by plan and invite teammates, with isolation enforced for you."
section: "guides"
status: "Building for 0.3"
spec: "tenancy"
canonical: "https://mainsdk.dev/docs/guides/build-a-multi-tenant-app"
updated: "2026-10-28"
---

# Build a multi-tenant app

> Give every customer a tenant, issue API keys, gate features by plan and invite teammates, with isolation enforced for you.

Status: Building for 0.3 · Canonical: https://mainsdk.dev/docs/guides/build-a-multi-tenant-app · Updated: 2026-10-28

You will turn on tenancy, let people create organisations and invite teammates, issue API keys for customer code, and gate a feature by plan. It takes about 30 minutes. The calls run on the in-memory fakes in tests, with the tenant directory held in memory.

## You need

- A MainSDK app with `auth` and `db` (see [Thirty-minute path](https://mainsdk.dev/docs/getting-started/thirty-minutes)).
- A `pay` subscription, if you gate by plan.

## 1. Turn on tenancy

```yaml title="main.yaml"
tenancy:
  isolation: shared
  resolve: [apikey, session, subdomain]
  personal: true
  roles: [owner, admin, member, viewer]
  system: { allow: [src/admin/**, scripts/**] }
```

Each person gets a personal tenant at first sign-in. Every request now has a tenant, and every `db` query is scoped to it. Never pass a tenant id by hand. Organisations and invites also need `auth` configured (a key and a method, see [Sign-in and sessions](https://mainsdk.dev/docs/interfaces/auth)).

## 2. Create an organisation and invite a teammate

```ts title="app/api/orgs/route.ts"
import { auth } from "mainsdk";

const org = await auth.orgs.create({ name: "Acme", region: "eu" });   // the creator becomes owner
await auth.invites.create({ email: "jane@acme.com", roles: ["member"] });
```

`auth.invites.create()` acts on the current tenant, so the caller must be an owner or admin of an organisation (a personal tenant refuses invites). The invite is a getqr.id link, bound to Jane's email address, single use, and expires in 7 days.

## 3. Read the tenant in your code

```ts title="app/api/orders/route.ts"
import { auth, models, s, tenant } from "mainsdk";

const { orders } = models({
  orders: s.object({ item: s.string(), total: s.int() }),
}); // declared once in lib/orders.ts, shown inline so the example stands alone

export async function GET(req: Request) {
  const { user } = await auth.require(req);
  const t = tenant.current();   // { id, kind, region, plan, roles, ... }
  const page = await orders.for(user).list({ limit: 20 });   // only this tenant's rows
  return Response.json({ tenant: t.id, items: page.items });
}
```

In a job, wrap the work: `tenant.run(id, fn)`. Use `tenant.system(reason, fn)` only in the admin paths you listed under `tenancy.system.allow`.

## 4. Issue an API key

Creating a key needs a role that holds `apikeys.manage`, and the scopes you grant must be declared under `apikeys.scopes`.

```yaml title="main.yaml"
access:
  roles:
    owner: [apikeys.manage, orders.read]
apikeys:
  scopes: [orders.read]
```

```ts title="app/api/keys/route.ts"
import { apikeys } from "mainsdk";

const { secret } = await apikeys.create({ name: "CI", scopes: ["orders.read"] });   // shown once
```

Add `apikey` first under `tenancy.resolve`, and a request carrying the key gets that tenant. Never hand-roll key storage.

## 5. Gate a feature by plan

```yaml title="main.yaml"
plans:
  free: { features: [basic], limits: { projects: 5 } }
  pro:  { features: [basic, export], limits: { projects: 50 }, prices: [price_pro_monthly] }
entitlements: { default: free }
```

```ts title="app/api/export/route.ts"
import { entitlements } from "mainsdk";

entitlements.require("export");   // throws MAIN-PLAN-0001 (403) with an upgrade hint on the free plan
```

## Check it worked

`npx mainsdk check` passes. A request with tenant A's key cannot read tenant B's orders: it gets a 404.

## If it fails

| Code | Fix |
| --- | --- |
| `MAIN-TENANT-0001` | A job ran with no tenant. Wrap it in `tenant.run(id, fn)` |
| `MAIN-TENANT-0004` | `tenant.system()` ran in a file outside `tenancy.system.allow`. Move it, or list the path (`mainsdk check` reports the same as `MAIN-TENANT-0011`) |
| `MAIN-PLAN-0001` | The tenant's plan lacks the feature. Move it to a plan that has it |
| `MAIN-PLAN-0010` | A feature no plan declares. Add it under `plans` |

## Next

- [Tenants](https://mainsdk.dev/docs/interfaces/tenant)
- [API keys for your customers](https://mainsdk.dev/docs/interfaces/apikeys)
- [Plans and entitlements](https://mainsdk.dev/docs/interfaces/entitlements)
