




2938 Members
The right authorization design is hybrid by default: RBAC for organizational roles, ABAC for contextual gates, and ReBAC for ownership, sharing, and hierarchy. Don't turn access control into a model war. Pick the model that matches the question, enforce it through one check API, and keep policy outside application code.
Authorization debates often start in the wrong place.
Someone says RBAC is too limited. Someone else says ABAC is too complex. Then ReBAC enters because graph-based authorization is the right answer for sharing, hierarchy, and collaboration. Very quickly, the discussion becomes religious.
That is not how production SaaS gets built.
Real products have different access questions at the same time. Is this user an admin in this organization? Can this request touch production data? Was this file shared with this user's team? Can this billing manager approve an invoice only under a threshold?
Those are not the same question. Forcing one model to answer all of them is how authorization systems become brittle.
RBAC gives you organizational structure. ABAC gives you runtime context. ReBAC gives you relationships between users, resources, groups, tenants, folders, projects, and teams.
The mistake is not choosing RBAC, ABAC, or ReBAC. The mistake is forcing one of them to do every job.
Authorization should be designed as a stack. Product teams should express policy using the model that matches the access question, while application developers keep calling the same enforcement path:
can(principal, action, resource, context)
The application should not care whether the decision came from a role, an attribute rule, a relationship, or all three. The policy plane can evolve; the enforcement interface should stay stable.
Each authorization model exists because it solves a real class of problems. The goal is not to rank them. The goal is to use each where it is strongest.
Role-Based Access Control is best for organizational roles and coarse job functions.
RBAC answers questions like: is Alice an admin in Acme Corp, and can tenant admins invite users?
This is the model most teams start with because it maps well to how companies think about responsibility. People have jobs. Jobs imply permissions. Roles are easy to explain to business stakeholders, customer admins, and security teams.
In a multi-tenant SaaS application, RBAC usually shows up as tenant-scoped roles: owner, admin, member, viewer, billing admin, support agent. That is a healthy use of RBAC. It is simple, understandable, and operationally manageable.
The problem starts when RBAC becomes the only tool in the box. A role should represent responsibility, not encode every condition, exception, ownership path, environment, and customer-specific rule.
Concrete example: a billing_admin role in a tenant can view invoices, update payment methods, and manage billing contacts. That is RBAC doing its job.
Permit supports RBAC as a first-class policy model, including roles, permissions, tenants, resources, and actions. The docs cover the core concepts here: RBAC overview.
Use RBAC when the access question is about organizational responsibility. Do not use it to hide every contextual or resource-specific rule inside a growing list of roles.
Attribute-Based Access Control is best for context.
ABAC answers questions like: is the user in the same tenant as the resource, is the request coming from an approved environment, is the resource classified as sensitive, or is the transaction amount below an approval threshold?
ABAC is powerful because it avoids creating a new role for every variation of a condition. Instead, policy evaluates attributes about the principal, resource, action, and request context.
Concrete example: a support agent may view a customer record only if they are assigned to the account, the request comes through the support console, and the record is not marked restricted. That is not a new role. That is context across principal, resource, and request attributes.
Permit supports ABAC so teams can build policies over user, resource, tenant, and runtime attributes without hardcoding those checks across services. See the ABAC overview.
Use ABAC when the question is conditional. Do not use it as a substitute for relationships. If access flows through ownership, group membership, inheritance, or sharing, you are modeling a graph.
Relationship-Based Access Control is best for sharing graphs, ownership, hierarchies, and instance-level access.
ReBAC answers questions like: is Alice the owner of this document, was this file shared with Bob directly, is Bob a member of a team that has access to this project, or does this folder inherit access from its parent workspace?
Collaborative SaaS access is often about how a user relates to a specific object: owner of document, member of team, team viewer of folder, folder parent of file. Concrete example: Alice is an editor on one document because it was shared with her team, a viewer on another via the parent workspace, and has no access to a third. Encoding that as RBAC creates role explosion immediately.
Permit supports ReBAC for relationship-based policies and graph-style authorization decisions. The model details are here: ReBAC overview.
Use ReBAC when the question is about a relationship between entities. Do not make every relationship a role.
RBAC gets you far. Then one day it hits a wall.
The wall usually looks like roles that started clean and became overloaded: admin, project_admin, project_admin_eu, project_admin_eu_prod, project_admin_eu_prod_sensitive, project_admin_eu_prod_sensitive_readonly.
Nobody designs this intentionally. It happens one enterprise requirement at a time.
Enterprise exceptions arrive one at a time—region, environment, sensitivity, delegated admin, project-level read-only—and each feels small enough to solve with another role. Eventually roles stop representing jobs and start representing combinations of conditions. That is the RBAC wall.
You know you are there when role names become long and specific, engineers fear deleting old roles, customer admins cannot explain grants, and access reviews stall because entitlements encode multiple concepts and historical exceptions. RBAC is not the problem; RBAC is being asked to do ABAC and ReBAC work. Model those dimensions explicitly instead of hiding them in roles or application code.
The pragmatic answer is hybrid by default.
Not hybrid as in "make everything complicated." Hybrid as in "each access question has a natural model."
Use RBAC for organizational responsibility. Use ABAC for contextual gates. Use ReBAC for ownership, sharing, inheritance, and hierarchy.
A tenant admin managing users inside a tenant is mostly RBAC with tenant scoping. A support agent viewing a restricted record only through an approved workflow is RBAC plus ABAC. A user opening a file shared with a group inside a workspace is ReBAC. A project maintainer approving production changes only from a trusted environment is ReBAC plus ABAC.
This is how mature authorization systems behave. A single decision may require a role, a relationship, and a runtime condition to be true.
For example:
A support engineer can view a customer ticket if they have the
support_agentrole, are assigned to the customer account, the ticket is not legally restricted, and the request is made through the support console.
That decision includes RBAC for the job function, ReBAC for the assignment, and ABAC for sensitivity and request origin.
Trying to force that into one role creates a bad role. Trying to hardcode it in the app creates authorization sprawl. The right answer is to express the policy in the policy plane and enforce it through a stable API.
Permit's policy basics are a good starting point for understanding how resources, actions, roles, conditions, and policies fit together: policy basics.
Application code should not be the place where authorization models fight.
Your application needs a simple enforcement contract:
await permit.check(user, "approve", invoice)
Or, more generally:
can(principal, action, resource, context)
The application is asking one question: can this principal perform this action on this resource in this context?
That call happens at the Policy Enforcement Point, or PEP. The PEP is where the application enforces the decision—API middleware, service, resolver, job, or gateway. Policy logic belongs in the policy plane: roles, relationships, conditions, tenants, and resource types. The Policy Decision Point (PDP) evaluates that policy.
Hardcoded authorization does not scale. Scattered checks turn every policy change into a deploy and every audit into code archaeology. Permit's hybrid PDP model keeps decisions close to the app for latency and resilience while policy is managed centrally. OPAL distributes policy and authorization data so PDPs stay current without centralizing sensitive application data just to decide access.
Today the decision may be RBAC; tomorrow ABAC; next quarter ReBAC for sharing. The app should still call can(principal, action, resource, context). That is the difference between an authorization system and a pile of permission checks.
Least privilege is not a slogan. It is a combination of model and enforcement.
You need a model that expresses who should have access, under what conditions, and through which relationships. Then you need enforcement points that consistently apply that model across the product.
Coarse RBAC often fails during access reviews for a simple reason: reviewers cannot explain entitlements.
If someone has admin, what does that actually mean? If someone has regional_admin_sensitive_prod_override, why do they have it? If a contractor can open a customer record, is it because of their role, their assignment, a temporary exception, a shared project, or a stale permission?
When reviewers cannot answer those questions, the review becomes weak. People either approve access they do not understand or escalate every unclear entitlement to engineering.
Hybrid authorization becomes a compliance bridge because it gives structure to least privilege. Not because a framework mandates a specific mix of RBAC, ABAC, and ReBAC. The point is simpler: access needs to be explainable.
RBAC explains job responsibility. ABAC explains conditions. ReBAC explains object-specific relationships. Decision logs explain what actually happened.
That last part matters. Policy is not enough if you cannot inspect decisions. When a customer asks, "Why was this allowed?" or "Why was this denied?" you need an answer grounded in evaluated policy, not a Slack thread and a grep across services.
We wrote more about why teams should stop burying permissions inside application code here: Stop rebuilding permissions.
We also wrote about authorization logs and explainable decisions here: Why was this allowed? Authorization logs.
The compliance takeaway is straightforward: model access in a way humans can understand, enforce it consistently through PEPs, keep decisions explainable, avoid coarse roles that hide too many reasons inside one entitlement, and avoid hardcoded exceptions that never show up in policy review.
That will not automatically make an audit pass. But it gives security, engineering, and compliance teams a much better foundation than "there is an isAdmin check somewhere in the codebase."
Before modeling permissions, ask these questions in order.
Is this access based on a job function or tenant responsibility?
Use RBAC: organization owner, workspace admin, billing manager, viewer.
Is this access based on a runtime condition or property?
Use ABAC: environment, region, sensitivity, time, plan, workflow state, authentication strength.
Is this access based on a relationship to a specific object?
Use ReBAC: owner of document, member of team, maintainer of project, inherited folder access, shared with group.
Would adding a new role encode more than one idea?
Stop. You are probably about to create role explosion. Split the concern into role, attribute, or relationship.
Can a customer admin explain this permission without reading code?
If not, the model is too implicit. Make the entitlement visible in policy.
Can security explain why a decision was allowed or denied?
If not, you need clearer policy structure and better decision logs.
Will this rule change per customer or be enforced in multiple services?
If yes, externalize it. Enterprise authorization requirements should not require custom forks or copied conditionals.
Does this access depend on fresh authorization data?
If yes, design for policy and data distribution to PDPs. Stale authorization data can become a security bug.
Good authorization architecture should be boring. The complexity belongs in the policy model and decision engine, not scattered across controllers and services.
Permit.io is built for this hybrid reality.
We do not believe teams should choose between RBAC, ABAC, and ReBAC as if one model will solve every authorization problem forever. Real SaaS products need all three.
Permit gives teams multi-model policy (RBAC, ABAC, ReBAC), one stable check path, policy externalization, hybrid PDPs close to the app, OPAL-based realtime policy/data distribution, and decision visibility for debugging. Start with the model that matches your current question:
The right question is not "RBAC vs. ABAC vs. ReBAC?" The right question is: what is the access decision I need to explain, enforce, and evolve?
If it is an org responsibility, use RBAC. If it is context, use ABAC. If it is ownership, sharing, inheritance, or hierarchy, use ReBAC. Then enforce all of it through one check API.
Permit.io is SOC 2 Type II certified and HIPAA, GDPR, CCPA, and ISO 27001 compliant; ISO 27001 is compliance, not certification, and your implementation still needs the right controls and processes.
If you are hitting the RBAC wall, start with the docs or try Permit and model the stack instead of rebuilding permissions again.

Co-Founder / CEO at Permit.io