
Role-Based Access Control (RBAC) for B2B SaaS - Part 2
Part 2 of 2. Start with Role-Based Access Control (RBAC) for B2B SaaS.
How do you implement role-based access control (RBAC) in a multi-tenant B2B SaaS application?
Scope every role to a tenant rather than to the user globally, carry the active tenant in session state, and enforce on the server in the code that reads or mutates the data. The role table is the easy part: the work sits in role explosion, and in the gap between changing a role and that change reaching a token already in a user's browser. This guide covers both, then the three ways to implement RBAC (in your own database, in a dedicated authorization service, or in your auth provider), and a checklist to run before you ship.
This part covers the multi-tenant failure modes (role explosion, token staleness and auditability), those three implementation approaches, and Clerk's organizations model. Part 1 covers what RBAC is, the model levels and their standards provenance, and RBAC versus ABAC, ACLs and ReBAC. Neither part covers pricing, per-framework implementation code, or the full Clerk and Auth0 feature comparison.
RBAC for B2B SaaS: why multi-tenant access control is harder
Single-tenant RBAC is a solved problem. Multi-tenant RBAC changes the unit of the decision from a role to a role inside a tenant, and role explosion, stale tokens and audit difficulty all follow from that one change.
Multi-tenant RBAC: scoping roles to the tenant
In B2B SaaS a role is meaningless without a tenant. The same person is an Admin in one workspace and a read-only Member in another, in the same session. A globally scoped role model cannot express that, and reaching for one is the most common early architectural mistake in multi-tenant products.
Three consequences follow. Every authorization check needs a tenant in scope, and the active tenant has to be part of session state. Switching tenants must not carry the previous tenant's permissions forward.
AWS Prescriptive Guidance names the risk: "A hypothetical user could be authenticated and authorized, and still access the resources of another tenant." Tenant isolation is what blocks it.
Microsoft's Azure Architecture Center lists five identity antipatterns, among them user and tenant conflation and role and resource authorization conflation. Its companion guidance states the requirement in one line: "a user might have administrative rights in one tenant but limited access in another tenant."
For the front-end side of multi-tenancy, see multi-tenancy in React applications.
Role explosion and how to prevent it
Role explosion is what happens when role count grows combinatorially: each customer asks for a slightly different bundle, until there are more roles than real distinctions between them.
NIST names it, rather than any vendor. SP 800-162 section 2.1 records "what is often termed 'role explosion'", and NISTIR 7316 section 3.4.5 states the tension underneath it: "For stronger security, it is better for each role to be more granular... For easier administration, it is better to have fewer roles to manage."
Two numbers show its shape. GitLab's design documentation records "over 700 permissions in the gitlab-org/gitlab codebase," with its permissions documentation carrying a table of over 200 rows. And in an enterprise access dataset analysed by Ene et al. at SACMAT '08 (the ACM record is paywalled; the full text is open access), 35 users holding 3,046 permissions had a provably minimum role set of 34 roles, roughly one role per user. Finding that minimum is NP-hard.
Two mitigations work: role hierarchies, covered in Part 1, and per-tenant role sets that offer each customer only the roles that make sense for them. Both trade simplicity for flexibility.
How many roles is too many? Shipped products cluster low, with GitHub documenting seven predefined organization roles. If your customers cannot explain the list back to you, it is too long.
Roles in session tokens: propagation and revocation
If roles and permissions ride in a session token, a role change is not effective until that token is refreshed or replaced. Design the revocation path alongside the role model.
This is a property of token-based authorization rather than a vendor flaw. Microsoft Entra's continuous access evaluation documentation records that administrator changes to Conditional Access policies and group membership "could take up to one day to be effective," with optimization reducing some policy updates to two hours, and that critical event evaluation may still show up to 15 minutes of propagation latency.
Token lifetimes set the worst case. An Auth0 access token for a custom API "is valid for 86400 seconds (24 hours)" by default; a Clerk session token is 60 seconds by default, refreshed on a 50-second timer.
NIST SP 800-63B-4 section 5.1.2 states the rule: "The access token and any associated refresh tokens could be valid long after the authentication session has ended and the subscriber has left the application." Section 5.2 adds that sessions at multiple relying parties "are established and terminated independently of each other," so concurrent sessions multiply it.
Shortening lifetimes is a partial answer. Microsoft records that they "experimented with the 'blunt object' approach of reduced token lifetimes but found they degrade user experiences and reliability without eliminating risks." Okta principal security engineer Maria Vasilevskaya writes that access tokens are self-contained JWTs and "we cannot instantly revoke them upon a security event." The standards-layer answers are OIDC Back-Channel Logout and OpenID CAEP 1.0, whose token-claims-change event exists because a role change has to reach tokens already issued.
Design for three cases: a role change, a deactivation or deletion, and a user holding several concurrent sessions. Check what your provider documents for each, and whether it can force a token refresh, rather than assuming a role edit invalidates anything.
Auditability and access reviews
RBAC has to answer one question under audit: who had access to what, and when. A model that cannot enumerate effective permissions cannot answer it, and the standard leaves permission-role review optional, as Part 1 covers.
The economics point the same way. The RTI International analysis prepared for NIST found that for a hypothetical 10,000-employee firm, provisioning benefits totaled about $322,000 a year, roughly $24,000 in IT labor plus $298,000 in reduced employee downtime, while policy maintenance and certification accounted for roughly $1.1 million, in 2009 dollars. RBAC's measured payoff is auditability.
AICPA TSC points of focus under CC6.2 and CC6.3, and NIST SP 800-53 AC-2(j) and AC-6(7), call for periodic access review, while the criteria and base controls themselves leave the cadence open. PCI DSS v4.0.1 quantifies it: Requirement 7.2.4 sets access reviews "at least once every six months," and Requirement 10.5.1 sets 12 months of audit log retention with three months immediately available.
How to implement RBAC: three approaches
Three options, and the choice usually follows from how relationship-shaped your permissions are.
Build RBAC in your application database
The classic shape is users, roles, permissions, and join tables between them. You get full control and no vendor coupling, and a flat model is trivial to write.
The initial schema is easy. The tripwire is tenant scoping and revocation added a year later, which is a migration rather than a patch, touching every join and every cached check.
Figma, Airbnb and Carta have published rebuilds, and Slack has written up a granular roles service running alongside its legacy predefined roles. Figma's account puts permission checks at around 20% of total database load before its rewrite. Read that evidence as skewed: teams write up the rewrites, not the schemas that kept working.
Use a dedicated authorization service
A separate engine holds the policy and answers checks. OpenFGA, SpiceDB, Cerbos and Ory Keto are Apache-2.0 engines you can self-host, and Permit.io is a managed control plane over Open Policy Agent and Cedar. All five fit fine-grained, relationship-shaped or cross-service authorization.
The cost is measurable. Google's Zanzibar paper reports Check at p95 9.46 ms against a settled consistency token and 60.0 ms when the check must reflect recent writes, with writes at 127 ms median. The zookie contract behind those numbers means storing a Zanzibar-issued token with each content version in an atomic client write and sending it on later checks, so it changes your schema, write path and read path.
The open-source cores are healthy, the companies around them less uniformly so. Aserto announced in April 2025 that it would shut its hosted service "by May 31, 2025" and "wind down Aserto as a commercial entity," leaving a set of maintainers to carry Topaz. In August 2025 the OPA creators told their community that they and "many team members from Styra" had joined Apple, and that Styra's commercial tooling had entered the community process to join the CNCF OPA GitHub organization. OPA itself "remains a CNCF graduated open source project and there are no changes to the project governance or licensing."
Use RBAC from your authentication provider
If roles are already per-organization and the session already carries the tenant, the provider issuing that session is a reasonable place to keep them. OWASP's Session Management Cheat Sheet lists "role, privilege level, access rights" among the details a server-side session object can hold, and RFC 9068 section 2.2.3.1 says an authorization server that includes such attributes SHOULD use RFC 7643's groups, roles and entitlements claim types.
Two qualifications apply. Co-location does not remove staleness, since a role change still has to reach an issued token, and Microsoft's Azure Architecture Center presents storing authorization data in your identity provider or in your application as two options.
The ceiling you hit in practice is claim size rather than role count. Firebase caps custom claims at 1,000 bytes, Entra drops the groups claim entirely above 150 groups for SAML and 200 for JWT, replacing it with an overage claim pointing at Microsoft Graph, and ASP.NET Core's docs warn that "a role claim may be too large to be contained within a token usable by a SPA." Auth0, WorkOS, Stytch, Descope, Keycloak, FusionAuth and Entra External ID ship named roles in the token, and Amazon Cognito surfaces user pool groups in a cognito:groups claim your application maps to roles.
RBAC with Clerk: organizations, roles, and permissions
Clerk ships the flat model with the tenant boundary built in, which makes it a concrete example of tenant-scoped roles, a tenant carried in session state, and enumerable permission strings.
Organizations as the tenant boundary
Organizations are Clerk's tenant primitive. A user belongs to one or more organizations, holds a role in each, and the session carries an active organization, so every authorization check has a tenant in scope by construction.
A role is meaningless without a tenant, and the organization is the unit membership hangs off, rather than a tenant_id you add to your own role tables and remember to filter on in every query. The Owner, Admin, Member and Billing Manager workspace worked through in Part 1 maps onto it directly.
Clerk's multi-tenant architecture guide covers how the active organization is resolved and switched, and the organizations overview covers membership, invitations and domains. Switching organizations issues a new session token carrying the new active organization, which is what stops the prior tenant's permissions carrying forward.
Multiple tabs complicate that. Each tab keeps its own active organization, but the session token cookie is "a singleton (global) value for the browser" that always represents the active tab's organization, so a background fetch started from an unfocused tab can carry the wrong tenant. Clerk's guidance is to not rely on the session cookie alone for background work: call getToken() and send the result in the Authorization header, which returns a token scoped to the focused tab's organization.
That is a correctness fix, not the security boundary. The server still has to bind the organization ID it verified from the token to the tenant that owns the record, on every read and every mutation, before it returns or writes anything.
Roles, permissions, and the org:<feature>:<permission> format
Clerk ships two default roles per instance: org:admin, which holds all System Permissions and can fully manage the organization, and org:member, limited by default to reading members and billing. Beyond those you can add custom roles. The roles and permissions docs state you "can create up to 10 custom Organization Roles per application instance," with the qualifier in the same breath: "Creating custom roles in development mode is free. In production, custom roles require the B2B Authentication add-on."
Role Sets are the per-tenant mitigation for role explosion in concrete form: "collections of available Roles you can assign to members in an Organization," so a role missing from an organization's set cannot be assigned to its members. The Primary Role Set is free, and additional sets require the B2B Authentication add-on.
Permissions follow the org:<feature>:<permission> convention, for example org:invoices:create. Predictable, enumerable permission strings make permission-level review possible. The model is flat: Clerk's organizations documentation describes no role inheritance mechanism, which is what puts it at flat, or Core, RBAC in Part 1.
One caveat matters for server code. System Permissions, the org:sys_ prefixed ones, "aren't included in session claims. If you need to check Permissions on the server-side, you must create Custom Permissions for authorization checks in your application code." The o.rol and o.per claims ship in the session token with no JWT template required, and o.rol carries the Role without its org: prefix, so org:admin reads as admin. Clerk documents their parent o claim as present only when the user belongs to an Organization and that Organization is active.
Roles and Permissions are both configured in the Dashboard under Roles & Permissions. Roles & Permissions: Role sets sits alongside it and requires the per-organization role sets feature.
Checking permissions: server-side vs client-side
Client-side permission checks are a UX affordance; the server-side check is the security boundary. You need both, and access-control bugs come from confusing the two.
Put the server check in the code that reads or mutates the data, meaning the server component, route handler, or server action, rather than only in a request-interception layer. Clerk's clerkMiddleware() reference is explicit: "Middleware is not the best place to protect routes. Instead, protect access as close to the resource as possible, in the code that reads or mutates the data." Next.js says the same about its own proxy layer: "Always verify authentication and authorization inside each Server Function rather than relying on Proxy alone."
In Clerk, has() is the recommended server-side check, with auth.protect() as the alternative when the request should terminate. Clerk documents authorization checks as likely to evaluate to false by default when no Organization is active, so a tenant-scoped check needs one selected. The authorization checks guide has the full pattern.
On the client, <Show> conditionally renders UI, and Clerk is blunt about its limits: it "only visually hides its children," so "Do not use this component to hide sensitive information that should be completely inaccessible to unauthorized users." For worked implementations, see RBAC in Next.js and authentication for a Python backend.
How Clerk and Auth0 approach RBAC
Both platforms ship organization-scoped roles and permissions, Auth0's in Early Access and permitted in production. The dimension where they diverge is relationship-based authorization: Auth0 FGA is a ReBAC engine built on OpenFGA, Clerk has no equivalent, and graph-shaped permissions are where Auth0 is the better fit. Part 1 covers what ReBAC is.
For the full feature-by-feature comparison, including default role keys, custom-role ceilings, token claims and enterprise SSO, see Clerk vs Auth0 and can Clerk handle enterprise requirements.
RBAC implementation checklist
Run through these before you ship a role model. Each one is a decision that is cheap now and an expensive migration later.
Building on Clerk? Start with organizations and the roles and permissions guide. For the full vendor comparison, see Clerk vs Auth0. If your permissions turn out to be relationship-shaped, look at a dedicated engine before stretching roles to cover it.
In this series
- Role-Based Access Control (RBAC) for B2B SaaS
- Role-Based Access Control (RBAC) for B2B SaaS - Part 2 (you are here)