Skip to content
SaaS Development2 June 2026 · 7 min readUpdated 30 September 2026

Designing Role-Based Access Control (RBAC) for Multi-Tenant SaaS

How to design RBAC for a multi-tenant SaaS so roles, tenant isolation and the data layer agree, with the six-role Callidus build, JWT claims, and a negative-path test list.

Designing Role-Based Access Control (RBAC) for Multi-Tenant SaaS

What you'll learn

  • How does multi-tenant RBAC differ from single-tenant RBAC?
  • Where should the access decision actually live?
  • How do JWT claims carry the tenant and role?
Article contents

Multi-tenant RBAC means every permission check answers two questions at once: is this user a member of this tenant, and does their role in that tenant allow this action? If you're about to add roles to a SaaS that already has tenants, write the tenant-isolation rule first and layer roles on top of it. Hiding a button in the frontend is presentation, so the real decision has to sit where the data lives, where a hand-crafted API call can't walk around it.

This post is the access-control companion to the broader Multi-Tenant SaaS Architecture guide. There, the question is how tenants share infrastructure. Here, the question is who inside a tenant is allowed to do what. The code samples are illustrative patterns, not copies of any production rules file.

How does multi-tenant RBAC differ from single-tenant RBAC?

In a multi-tenant app, a role only has meaning inside one tenant, so the unit you authorize is a membership: a user, a tenant and a role together. A tenant admin is an admin in their own tenant and has no rights in anyone else's.

NIST describes the classic model in one sentence: each user is assigned one or more roles, and each role is assigned one or more privileges permitted to users in that role. Multi-tenancy adds a second axis on top. The consequence is that a role can't be a single column on the user record. One person can be an admin in one workspace and a read-only viewer in another, so the role lives on the membership row that links the two.

If you're still deciding how tenants are physically separated, read Database-per-Tenant vs Shared Schema for SaaS first, because that choice changes where your checks run.

Where should the access decision actually live?

It should live in the data layer, enforced by the database or its rules engine, because any check that lives only in the UI or a single middleware can be skipped. OWASP's 2025 Top 10 keeps Broken Access Control at number one and reports that 100% of the applications tested had some form of it.

That statistic is about the tested sample, not about every app in the wild, but the direction is clear. OWASP's prevention advice is blunt: except for public resources, deny by default, and enforce record ownership instead of letting users create, read, update or delete any record. The OWASP Authorization Cheat Sheet adds that permissions should be validated on every request, whether it came from an AJAX script, the server or anywhere else.

I apply that on Callidus, the UK aesthetic-clinic SaaS I built solo in React, TypeScript and Firebase. Per the case study, everything lives under tenants/{tenantId}/… in Firestore, the JWT claims carry tenantId and role, and every mutation passes an !isTenantArchived() guard in the Firestore rules. The product has six roles: super admin, owner, admin, manager, practitioner and receptionist. The code is built around role helpers (hasClinicalAccess, hasManagerAccess, hasAdminAccess) instead of inline role checks, so a change to the access matrix propagates consistently. The case study also lists 100% Firestore rules coverage. The subscription grace period shows why the rules layer matters: pages stay viewable while mutations are blocked at both the route and the Firestore rules layers, so no single layer can be bypassed on its own. A UI-only check would leave exactly that gap.

Callidus uses Firestore rules, not Postgres RLS. For the engine-level trade-offs, see Firestore Rules vs Postgres RLS for Multi-Tenant SaaS.

What does a tenant-plus-role rule look like?

Here's an illustrative Firestore rule where the path carries the tenant and a custom claim carries the role. Firebase documents that custom claims are read in Firestore rules through request.auth.token.

match /tenants/{tenantId}/records/{recordId} {
  allow read: if request.auth.token.tenantId == tenantId;
  allow write: if request.auth.token.tenantId == tenantId
               && request.auth.token.role in ['owner', 'admin'];
}

One Firestore-specific detail matters for your tests. Firebase states that rules are not filters: a query is evaluated against its potential result set, and if it could return a document the client may not read, the whole request fails. A cross-tenant query therefore errors with permission denied rather than returning an empty list, and your client queries must include the tenant constraint themselves.

How do you test that RBAC actually holds?

Test the negative paths, because the happy path passing tells you almost nothing. The Authorization Cheat Sheet recommends unit and integration tests for access control logic, including deny-by-default behaviour. Three checks belong in every suite:

  1. Cross-tenant read. Sign in as a user of tenant A and request tenant B's record. The request must be rejected (Firestore) or return no rows (Postgres RLS).
  2. Role escalation. Sign in as a viewer and attempt a write. The rule must reject it, not just the UI.
  3. Forged tenant. Send a tenantId in the request body that differs from the token's claim. The body value must be ignored.

If you only test that an admin can do admin things, you've tested the part that was never going to leak. Run the same three checks again whenever you add a role, because a new role is a new way to be wrong. SaaS Security and Compliance covers the wider threat surface around tenant data.

How do JWT claims carry the tenant and role?

At sign-in, the auth system puts the tenant and role into the signed token so the data layer can read them on every request without an extra lookup. The two common Firebase and Supabase routes differ in where the role comes from and how fast a change takes effect.

Firebase custom claimsSupabase custom access token hook
Where the role is setAdmin SDK writes claims onto the userA hook runs before each token is issued and adds a claim such as user_role
Where it's readrequest.auth.token in rulesauth.jwt() inside RLS policies or an authorize() function
Size limitPayload must not exceed 1000 bytesNot stated on the page I read
Role change takes effectNext ID token issue: sign-in, expiry refresh, or forced refreshNot described on the page I read

Sources: Firebase custom claims and Supabase RBAC guide, both fetched 2026-09-30.

Two rules follow. First, claims are cached in the token, so a role change doesn't apply until a new token is issued. Firebase's docs say the client can force it with getIdToken(true), so call that after permission changes. Second, keep claims for access control only: Firebase warns that extra data in claims hurts performance because every authenticated request carries the token. Never trust a tenantId or role from the request body. For how the token is issued in the first place, see SaaS Authentication: Magic Links vs Passwords vs OAuth, and for a worked Auth.js setup, Auth.js v5 Multi-Tenant B2B SaaS.

How many roles should a new SaaS start with, and when is RBAC not enough?

Start with four coarse roles and split a permission only when a real customer workflow demands it. This is my recommendation, not a standard, and Callidus shows it isn't a ceiling: its six roles exist because clinical data, financial data, settings and billing each needed different access.

A workable starting ladder:

  • Owner. Billing, deleting the workspace, transferring ownership. Usually exactly one.
  • Admin. Manages members and most settings, but can't close the account.
  • Member. Does the daily work: creates and edits records.
  • Viewer. Read-only, for people who need visibility but shouldn't change anything.

A 20-permission matrix on day one feels thorough, but half of it never gets toggled and every entry is another branch your rules and tests must cover. When a paying customer says "I need someone who can edit bookings but not see revenue", split that permission out, because now you know what it's for.

RBAC also has limits. The OWASP cheat sheet says ABAC and ReBAC should typically be preferred for application development, because they express finer rules. NIST SP 800-162 defines ABAC as evaluating attributes of the subject, object, operation and environment against policy.

ModelDecision is based onFits when
RBACThe user's roleA handful of stable job functions
ABACAttributes of user, resource, action, environmentRules like "only during business hours, only own location"
ReBACRelationships between users and resourcesSharing, ownership and hierarchies between objects

What build order avoids rework?

  1. Model membership as its own record: (userId, tenantId, role). Don't put the role on the user.
  2. Write the tenant-isolation rule first and drive cross-tenant leakage to zero.
  3. Layer role checks on top, so every role check runs together with the tenant check.
  4. Put tenantId and role into the token as claims and refresh the token when permissions change.
  5. Write the three negative tests above before you ship.
  6. Add fine-grained permissions only when a real workflow asks for one.

Roles are recoverable, leaked data isn't, so get the isolation boundary airtight first. Your next step: pick one write endpoint in your app today and try to call it as a viewer from another tenant. If it succeeds, you've found your first rule to write.

Free resource

Free SaaS MVP Scope Template

A Notion document with the full feature checklist, MVP vs. nice-to-have table, pre-build questions, and cost signals — so you walk into any developer call knowing exactly what to ask for.

Get the template →
DL

Dusko Licanin

Full-Stack Developer · Banja Luka, Bosnia

Full-stack developer shipping SaaS MVPs, web apps, and mobile apps using AI-augmented workflows — without agency coordination overhead. Live portfolio: BookBed, Callidus, Pizzeria Bestek.

Frequently Asked Questions

What is multi-tenant RBAC?

Multi-tenant RBAC is role-based access control where every permission check also verifies tenant membership. A user holds a role inside a specific tenant, so the authorization unit is the triple of user, tenant and role, stored on a membership record rather than on the user. A tenant admin has admin rights only in their own tenant. The check has to run at the data layer, through database rules or row-level security, because UI-only checks can be skipped by anyone who can replay an API request with modified values.

Should RBAC be enforced in the frontend or the backend?

Enforce it in the backend, specifically at the data layer. Hiding UI elements from users without the right role is good UX, but it is not security, because anyone can replay a modified API request. OWASP advises denying by default and validating permissions on every request, whatever its source. Firestore security rules or Postgres row-level security are the usual places for that decision, since a forged request still has to pass them. Keep the frontend checks as a convenience layer on top.

How do you store a user's role in a multi-tenant SaaS?

Store it on a membership record that links a user to a tenant, as a (userId, tenantId, role) row, instead of a single column on the user. One person can hold different roles in different tenants, so the role belongs to the relationship. For fast checks, copy the tenant and role into the signed token as custom claims. Firebase limits the claims payload to 1000 bytes and applies changes the next time an ID token is issued, so force a refresh after permission changes.

Is RBAC enough for a SaaS, or do you need ABAC?

RBAC is enough when access follows a handful of stable job functions, and ABAC becomes useful when rules depend on attributes such as ownership, location or time. The OWASP Authorization Cheat Sheet says ABAC and ReBAC should typically be preferred for application development because they express finer rules, while NIST describes ABAC as evaluating attributes of the subject, object, operation and environment. A practical path is to start with four coarse roles and add attribute rules only when a real customer workflow needs them.