Designing Multi-Tenant RBAC for a SaaS
Multi-tenant authorization becomes much easier to reason about when identity, company membership, role and active tenant are treated as separate concepts. The mistake is usually not in the role names; it is in collapsing too many authorization decisions into the user record itself.
Start by separating identity from membership
A user answers who someone is. A tenant membership answers which company that person belongs to and what they can do there. Keeping those concepts separate makes multi-company access possible without duplicating users or overloading a single user.role field.
In practice, that means a user can have one identity and many memberships. Each membership belongs to one tenant and carries the role and company-specific preferences that apply only in that context.
User
id
email
TenantMembership
userId
tenantId
role
notificationsEnabled
UNIQUE(userId, tenantId)Roles belong to the tenant context
A role such as OWNER, ADMIN, AGRONOMIST, OPERATOR or VIEWER is meaningful only inside a company. The same person may be an owner in one tenant and a viewer in another.
That is why tenant-scoped roles should live on the membership, not on the user. A global role can still exist for platform-level concerns such as SUPERADMIN, but it should not be used as a shortcut for ordinary tenant authorization.
Resolve the active tenant explicitly
Every tenant-scoped request needs an active tenant. The backend should resolve it from a trusted session context and then verify that the user still has a valid membership for that tenant.
A useful fallback order is: requested tenant if the membership is valid, then the user's default company if still valid, then another valid membership. If no valid membership exists, the session should enter a no-tenant or onboarding state instead of silently reusing stale tenant data.
requested tenant
↓ valid membership?
default tenant
↓ valid membership?
first valid membership
↓
NO_TENANT / onboardingPut tenant context in the session, not in every client request
Once the active tenant is resolved, the server can issue a session or token carrying tenantId and the tenant-specific memberRole alongside any global platform role.
The client may request a tenant switch, but it should not be trusted to declare arbitrary tenant ownership on every API call. Authorization still comes from the server-side membership lookup and the session derived from it.
Use capabilities instead of scattering role checks
Direct checks such as if role === ADMIN quickly spread through controllers and become difficult to reason about. A capability matrix gives the application one place to define what each tenant role may do.
Controllers can then protect operations such as fields.write, team.invite, billing.manage or analytics.view without duplicating role semantics. This also makes exceptions explicit: for example, an ADMIN may manage fields and users but not billing, while an OWNER can manage both.
OWNER -> fields.*, team.*, billing.manage
ADMIN -> fields.*, team.*, billing.view
AGRONOMIST -> fields.read, alerts.*, analytics.*
OPERATOR -> fields.read, notes.write
VIEWER -> *.readEnforce both membership and capability
Authorization needs two checks: does the user belong to this tenant, and does the role for that membership allow the requested action? A capability check without tenant validation can authorize the right action in the wrong company.
I prefer centralized guards for tenant membership and member role or capability evaluation. That keeps controllers focused on application behavior while the authorization boundary stays consistent across fields, alerts, analytics, notifications, team management and billing.
Tenant switching is also a frontend state problem
Changing the active company is not complete when the backend returns a new tenantId. The client may still contain cached queries, selected filters, open forms or in-flight requests from the previous company.
When the tenant changes, tenant-scoped requests should be cancelled, caches invalidated or keyed by tenantId, and tenant-specific UI state reset. Otherwise a correct backend can still be paired with a frontend that briefly renders stale data from another tenant.
queryKey: ["fields", tenantId]
queryKey: ["alerts", tenantId]
queryKey: ["analytics", tenantId]Invitations must create membership, not alternate identity
Inviting someone to a company should converge on the same membership model whether the email already belongs to an existing user or to someone who still needs to register.
For an existing verified user, accepting the invitation can create the membership directly. For a new user, registration and email verification happen first, then the invitation creates the tenant membership. The invitation is temporary; the membership is the durable authorization record.
Removal and role changes must take effect immediately
Membership is dynamic. A user can be removed from a company or have a role downgraded while still holding an old session or an open browser tab.
Sensitive requests should therefore validate the current membership rather than trusting a long-lived client view of the role. The frontend should also refresh company and user context after team mutations so navigation and permissions do not remain stale.
No-tenant onboarding is a valid application state
A SaaS often needs to support a logged-in user who does not yet belong to a company. Treating this as an authentication failure makes onboarding awkward and encourages special cases.
A better design keeps the user authenticated but marks the session as having no active tenant. Only onboarding and company-creation routes are available until a tenant exists, after which the new membership becomes the active context.
The boundary to protect
The most important rule in multi-tenant RBAC is that every tenant-scoped read and write must derive its tenant from trusted authorization context. Filtering by tenantId is not merely a query concern; it is part of the security boundary.
A good model makes the correct path easier than the dangerous one: membership is explicit, roles are contextual, capabilities are centralized, sessions carry resolved tenant context and client caches are tenant-aware. The result is easier to extend and much harder to misuse accidentally.
Related work
See the multi-company authorization model in the Vienor case study