tenant config in createAuth() enables:
- Tenant-scoped JWTs — each session is scoped to one tenant at a time
- Tenant switching — users switch between tenants without re-authenticating
- Tenant listing — fetch all tenants a user belongs to
- Auto-resolve — automatically select the right tenant on login (last-accessed or custom logic)
- Last-accessed tracking — the server remembers which tenant the user last switched to
Quick start
Configuration
tenant.verifyMembership (required)
Called before every tenant switch. Return false to deny — the endpoint returns 403.
tenant.listTenants (optional)
Enables the GET /api/auth/tenants endpoint. Without this, the endpoint returns 404.
TenantInfo requires id and name. You can add any extra fields (logo, plan, role) — they pass through to the client:
tenant.resolveDefault (optional)
Determines which tenant to auto-select when a session has no tenantId (e.g., right after login). Called during GET /api/auth/tenants.
resolveDefault is not provided):
- Use
lastTenantId(the last tenant the user switched to) - Fall back to the first tenant in the list
- Return
undefinedif the user has no tenants
How it works
Tenant-scoped JWTs
When a user switches tenants, the server issues a new JWT withtenantId in the payload. All subsequent requests carry this scoped JWT. Entity access rules can use rules.where({ tenantId: rules.user.tenantId }) to filter data by the current tenant.
The tenantId is preserved across token refreshes — switching tenant once persists until the user explicitly switches again.
Last-accessed tracking
Every successful tenant switch updates the user’slastTenantId in the user store. This is used by the default resolveDefault strategy and is returned in the GET /api/auth/tenants response.
Endpoints
GET /api/auth/tenants
Returns the user’s tenants, current session tenant, last-accessed tenant, and the resolved default.
Response:
| Field | Type | Description |
|---|---|---|
tenants | TenantInfo[] | All tenants the user belongs to |
currentTenantId | string | undefined | Tenant in the current JWT (if any) |
lastTenantId | string | undefined | Last tenant the user switched to |
resolvedDefaultId | string | undefined | Recommended tenant to auto-select |
| Status | When |
|---|---|
401 | No valid session |
404 | listTenants not configured |
POST /api/auth/switch-tenant
Switches the session to a different tenant. Issues a new JWT scoped to the target tenant.
Request:
vertz.sid, vertz.ref) are set automatically.
Errors:
| Status | When |
|---|---|
401 | No valid session |
403 | User is not a member of the target tenant |
404 | Tenant config not enabled |
Entity tenant scoping
Entities with atenantId field are automatically scoped by the framework. The access system adds rules.where({ tenantId: rules.user.tenantId }) to all operations, so queries only return rows belonging to the current tenant.
Multi-level tenancy
Real-world SaaS apps often have multiple levels of tenancy — an account contains projects, or an agency manages organizations that contain brands. Vertz supports this natively: multiple.tenant() tables form a hierarchy, with billing, access, and data isolation at different levels.
Define the hierarchy
Mark multiple tables as.tenant(). The framework infers the hierarchy from FK relationships:
projects.accountId references accounts.id and infers: account → project (root → leaf). Up to 4 levels are supported.
Per-level billing
Plans get alevel field that targets a specific entity in the hierarchy:
group names (mutual exclusivity is per-level). Use defaultPlans (keyed by entity name) instead of defaultPlan for multi-level.
Feature resolution modes
When the same entitlement is gated at multiple levels, the framework supports two resolution modes:| Mode | Behavior | Use case |
|---|---|---|
inherit (default) | Parent plan grants features to children | Enterprise account unlocks features for all projects |
local | Only the deepest level’s plan is checked | Project must have its own plan to use a feature |
Cascaded wallet consumption
When consuming a limit at a child level, the wallet is incremented at every ancestor level that has a plan with the same limit. This enforces ceilings at all levels:- Project A wallet: +1 (of 500)
- Account wallet: +1 (of 10,000)
unconsume() mirrors this — decrementing all ancestor levels.
Level-aware tenant filtering
Entities scoped to a non-leaf level are filtered correctly. Ifbilling_invoices is scoped to the account level and the user is at the project level, the framework walks the ancestor chain to find the account ID:
Flag resolution
When the same flag exists at multiple levels, deepest wins (most specific overrides parent):Backward compatibility
Single.tenant() apps work unchanged — no level, no defaultPlans, no ancestorResolver. The multi-level features are entirely opt-in.
Session payload
Multi-level sessions includetenantLevel alongside tenantId:
switch-tenant endpoint resolves tenantLevel from the closure table automatically.
Next steps
Client-Side Tenants
TenantProvider, useTenant(), and TenantSwitcher component.
Authentication
JWT sessions, RBAC, plans, and full auth configuration.
Access Control
Use entitlements and access rules in your UI.