Multi-Tenancy
Single-tenant, shared multi-tenant, or fully isolated — Nest Auth supports all three.
Multi-tenancy is built into the core, not bolted on. The same APIs work whether you're single-tenant, supporting many tenants in one database, or running each tenant in its own database. You flip a flag rather than re-architect.
The three modes
tenant.enabled | tenant.mode | Storage | Best for |
|---|---|---|---|
false (default) | — | Single database, no tenantId column | Solo apps, internal tools, MVPs |
true | SHARED | Single database, every row carries tenantId | SaaS where tenants are cheap to spin up |
true | ISOLATED | Single database; identity is scoped per tenant (the same email is a separate account per tenant) | Apps where each tenant is a distinct org/"property" and accounts must never cross tenants |
SHARED mode
All tenants live in the same database. Every tenant-scoped table — nest_auth_user_accesses, your Order, your Invoice — carries a tenantId column. Every query the library issues filters by tenant. Every query you write should filter by tenant too.
Pros
- One database, one connection pool, simple ops.
- Cross-tenant queries trivially possible (super-admin views, support reports).
- Cheap to spin up a new tenant — one
INSERTintonest_auth_tenants.
Cons
- A bug in your tenant-filter query is a data-leak vulnerability. Use the request context consistently and write a repository wrapper that filters automatically if you can.
- Noisy-neighbour: a heavy tenant slows queries for everyone.
- Hard to satisfy compliance regimes that require physical isolation.
Where to filter by tenantId
The tenantId decorator reads from the AsyncLocalStorage-backed request context. The library populates it automatically based on the active session's tenant.
ISOLATED mode
What ISOLATED actually does: it scopes identity per tenant, in a single database. The same email is a distinct account per tenant —
alice@xin tenant A andalice@xin tenant B are two different users with two different password hashes, sessions, and role sets. Account uniqueness is(user, tenantId); the library looks users up asgetUserByEmail(email, tenantId). Because an account belongs to exactly one tenant,switchTenantis disabled in ISOLATED mode — to act as a different tenant you sign in to that tenant directly.
This is logical isolation (one database, tenant-scoped identity), which is what most "each customer is a separate org/property" apps need. Contrast with SHARED, where the same email is the same user who can belong to many tenants via nest_auth_user_accesses.
Logging in requires a tenantId — see Logging in under a tenant (ISOLATED). And because the same email can be several accounts on one device, ISOLATED pairs naturally with the account switcher.
The library does NOT switch databases for you. There is no per-tenant data-source/connection routing built in —
IsolatedTenantContextServiceonly resolves the activetenantId; it does not select a TypeORMDataSource. If you need physical per-tenant databases (separate connections/schemas for compliance), you wire that yourself (e.g. a request-scopedDataSourcekeyed bytenantId); the library gives you the tenant id on the request context to drive it.
Pros
- Strong logical separation — accounts and identities never cross tenants; the same email in two tenants is genuinely two accounts.
- Simple ops — still one database and one connection pool.
Cons
- Not physical isolation by itself — if you need separate databases for compliance, you add the data-source routing.
- A new account must be created per tenant (no implicit cross-tenant membership; that's SHARED's model).
Tenant context — how it gets resolved
Every authenticated request carries an active tenant. The library resolves it in this order:
- JWT claim — the access token carries
tenantId(set when the session was created). switchTenantaction (SHARED only) — explicit tenant switch viaPOST /auth/switch-tenantrotates the session. Disabled in ISOLATED.- Default tenant fallback — if
defaultTenantOptionsis configured and the user has no other tenant, the library puts them in the default.
At login the tenant is resolved differently — before there's a session: in ISOLATED you pass tenantId on the login/signup/forgot-password request (it's how the same email is disambiguated; the lookup is tenant-scoped). Use GET /auth/tenants/lookup?slug= to turn a human-friendly slug into the tenantId for the login form. See Logging in under a tenant (ISOLATED).
The resolved value lands on the request context and is what every @CurrentTenantId()-style decorator reads.
Default tenant
For apps that are "really single-tenant but might add tenants later":
The library auto-creates this tenant on first boot and assigns new signups to it (in registrationHooks.onSignup). You can flip tenant.enabled to false later if you decide tenancy was overkill.
Creating tenants programmatically
Use TenantService:
The TenantCreatedEvent fires automatically — wire your billing system, Slack notification, default-data seeder, etc. to the event listener:
Switching tenants
A user can belong to multiple tenants — the nest_auth_user_accesses table stores the membership. Switching is one call:
The server issues a fresh session bound to the new tenant; the client persists the new tenantId; React's useUser() re-renders with the new context. The tenant-switcher recipe shows a complete dropdown UI.
Roles per tenant
A user's roles are tenant-scoped by default. Alice can be admin in tenant A and member in tenant B. The NestAuthUserAccess row holds the per-tenant role assignment.
For roles that span all tenants — global admins, support staff — use Platform Access instead.
Multi-platform / guard pattern
A common production pattern is one backend serving several frontends — an admin console, a tenant-facing portal, a mobile app. Each frontend should only allow login from users with a role on its guard, even if the same backend serves them all.
This is documented in detail in the multi-platform login recipe.
Where tenancy lives in the schema
| Table | Purpose |
|---|---|
nest_auth_tenants | One row per tenant (name, slug, isActive, metadata) |
nest_auth_user_accesses | Many-to-many between users and tenants, with per-tenant roles |
nest_auth_platform_accesses | Cross-tenant roles for staff (no tenant column) |
Your tenant-scoped tables (orders, invoices, …) | Carry a tenantId column. (If you add physical per-tenant databases on top of ISOLATED, you route the connection yourself — see ISOLATED mode above.) |
Decorators
| Decorator | Returns |
|---|---|
@CurrentTenantId() | The active tenantId (string) |
@CurrentTenant() | The full NestAuthTenant entity |
@CurrentUserAccess() | The current user's NestAuthUserAccess for this tenant |
@CurrentMembership() | Alias for @CurrentUserAccess() |
Related
- User Access & Platform Access — per-tenant vs cross-tenant roles.
- RBAC — roles, permissions, multiple guards.
- Logging in under a tenant (ISOLATED) — slug → tenantId, the login DTO, password reset.
- Multi-account login & switching — several ISOLATED accounts on one device.
- Tenant switcher recipe — SHARED-mode tenant switching.
- Multi-platform login recipe.
TenantServicereference.