Multi-Tenancy
Use namespaces and principal-derived scope to keep tenant data isolated by default.
Derive tenant scope from the authenticated principal and let namespaces enforce it automatically. No manual WHERE filters, no middleware that might be skipped, no convention that relies on every developer remembering to add a condition.
The Pattern
- Create an explicit tenant entity (usually
OrganizationorWorkspace). - Connect users to tenants through a membership entity.
- Project the current tenant into the principal.
- Declare a namespace that scopes entities to that principal field.
Every query against a namespaced entity is then filtered to the current tenant. Inserts automatically set the tenant column. There is nothing to remember and nothing to forget.
Complete Working Example
entity Organization {
name: string
slug: string @unique
}
entity OrgMembership {
orgId: Organization.id
userId: __User.id
role: string = "member"
@unique([orgId, userId])
}
auth {
providers: [email]
sessionDuration: 30d
principal {
orgId: Organization.id @selectFrom(OrgMembership.orgId)
}
}
namespace Tenant {
scope: principal.orgId
entities: [Project, Document, Comment]
}
entity Project {
name: string
description: string
ownerId: __User.id
@grant read, write to * via OrgMembership(userId)
@why("Organization members access shared projects.")
@grant read, write, delete to role(Admin)
@why("Admins have full access.")
}
entity Document {
title: string
body: string
projectId: Project.id
ownerId: __User.id
@grant read to * via OrgMembership(userId)
@why("Organization members can read documents.")
@grant read, write where resource.ownerId == principal.id
@why("Authors manage their own documents.")
}
entity Comment {
body: string
documentId: Document.id
authorId: __User.id
@grant read to * via OrgMembership(userId)
@why("Organization members can read comments.")
@grant write where resource.authorId == principal.id
@why("Authors can edit their own comments.")
}Only the auth fields needed for tenant selection are shown here. For the full auth model, including password policy, account linking, and the auth-service/runtime split, see Inspire Auth.
What Auto-Filtering Does
Once Project, Document, and Comment are inside the Tenant namespace, every interaction is scoped:
query(select(Project))getsWHERE tenant_id = $principal.orgIdautomatically. You never write this condition.query(insert(Project).values({ name: "New Project" }))sets the tenant column to the current principal'sorgIdautomatically. You do not pass it.- Includes and joins respect the same boundary. A query that includes
Documentrows for aProjectonly returns documents belonging to the same tenant.
There is no opt-out at the application level. This is a platform-level rule, not a convention.
Organization Switching
The @selectFrom(OrgMembership.orgId) annotation means the user chooses which organization to authenticate into. When a user belongs to multiple organizations, they select one at login time. The selected organization becomes principal.orgId for the duration of the session.
Server functions, policies, and browser queries all see a single tenant context per request. There is no ambiguity about which organization a query refers to.
If your app needs an organization switcher, the user re-authenticates with a different organization selection. The session is always bound to exactly one tenant.
Cross-Tenant Access
Some features genuinely need to reach across tenant boundaries: admin dashboards, support tools, analytics aggregations. Use system roles for these cases. System role functions bypass namespace scoping because they operate outside the normal principal context.
@system("support") {
displayName: "Support Agent"
}A support agent with this system role can query across tenants. This is intentional and auditable. System role usage appears in the security report.
Cross-tenant access is always explicit. Normal application code, queries, and policies all operate within the tenant boundary. Crossing it requires a system role, which is visible in the schema and reviewable during audits.
Namespace And Policy Interaction
Namespace scoping and authorization policies are two separate layers that evaluate in order:
- Namespace scoping happens first. The database engine filters rows to the current tenant before anything else. A user in Organization A cannot query Organization B's rows, regardless of what policy grants exist.
- Policy evaluation happens second. Within the tenant's rows, normal authorization rules apply: ownership, roles, junction access, deny rules.
Namespace isolation is a hard boundary. Even a @grant read to * only applies within the current tenant's data. It does not grant cross-tenant visibility.
Entities Outside The Namespace
Not everything in a multi-tenant app belongs inside the tenant boundary:
- The
Organizationentity itself defines the tenants and is not tenant-scoped. OrgMembershipconnects users to organizations and is queried during authentication, before a tenant context exists.- Shared reference data (countries, currencies, plan definitions) is the same across all tenants.
Only entities that contain tenant-specific business data belong in the namespace's entities array.
Recommended Design Decisions
Keep the namespace declaration close to the auth block. The tenant boundary should be obvious when someone reads the schema. Placing the namespace near the auth and principal configuration makes the isolation model easy to find.
Put all tenant-scoped entities in the namespace. If an entity logically belongs to a tenant, include it. Leaving an entity out means it will not get automatic tenant filtering, which creates an isolation gap.
Use membership entities for authorization within a tenant. Combine via grants with namespace scoping: the namespace guarantees tenant isolation, and the via grant controls who within the tenant can access what.
Do not store tenant IDs in application code. If you find yourself passing orgId through function parameters or storing it in React state for query purposes, the namespace is probably not configured correctly.
Test with multiple tenants early. Create two organizations in development and verify that queries from one never return data from the other. This catches misconfigured namespaces before they reach production.