You prompt Claude Code: "add role-based access control to my app." It generates a users table, a roles table, a middleware that checks permissions before each route. It works.
Then you build a second app. Claude Code generates another users table, another roles table, another middleware. Different schema. Different logic. No connection to the first one.
By the time you have five internal tools, you have five separate permission systems. Five places to define who can do what. Five places to update when someone joins, leaves, or changes teams. Five audit trails that do not talk to each other.
That is not RBAC. That is five islands pretending to have access control.
The real question is not "how do I add permissions to my app." It is "how do I design access control that works across every tool my team builds, today and next year."
Below: the actual RBAC model (the one NIST standardized), why per-app permissions fail, how RBAC compares to the alternatives, and what a centralized implementation looks like.
What RBAC actually is
Role-Based Access Control was formalized by David Ferraiolo and Rick Kuhn at NIST in 1992. It became an ANSI/INCITS standard in 2004 (INCITS 359), and it remains the most widely adopted access control model in enterprise software.
The idea is simple: instead of assigning permissions directly to users, you assign users to roles, and roles carry permissions. Alice does not get "can read invoices." Alice gets the "finance" role, and that role includes "can read invoices."
The indirection is the whole point. You manage permissions at the role level, not at the individual level. When a new person joins the finance team, you assign one role instead of 40 individual permissions. When someone leaves, you revoke one role.
The NIST model
NIST defines three levels of RBAC:
Core RBAC. Users, roles, permissions, and sessions. Users are assigned to roles. Roles carry permissions. A user activates a role in a session and gets the associated permissions. This is the minimum viable RBAC.
Hierarchical RBAC. Roles can inherit from other roles. A "manager" role inherits all permissions from "editor," which inherits from "viewer." You define each layer once, and changes propagate automatically. Without hierarchy, you duplicate permission sets across roles and they drift apart over time.
Constrained RBAC. Adds separation of duty rules. Static constraints prevent a user from holding two conflicting roles (the person who submits expenses cannot also approve them). Dynamic constraints prevent activating both roles in the same session. Separation of duties is required by SOC 2, ISO 27001, and Sarbanes-Oxley.
Most internal tool platforms, if they implement RBAC at all, stop at Core. They give you users and roles but no hierarchy and no constraints. That is enough for a prototype but not for production.
Why per-app RBAC fails
When Claude Code or Cursor generates access control for your app, it creates a self-contained permission system scoped to that single application. This feels right at first. But it breaks down fast.
No single source of truth for identity. Each app maintains its own user records. When someone leaves the company, you need to remember every app they had access to and disable them in each one. Miss one, and a former employee still has access three months later.
Roles diverge. App A defines "admin" as someone who can read and write everything. App B defines "admin" as someone who can also delete records and manage users. Same word, different meanings. Auditors hate this.
No cross-app visibility. An auditor asks: "What can this user access?" With per-app RBAC, you need to query five separate systems and reconcile the results manually. With centralized RBAC, it is one query.
No cross-app enforcement. An AI agent reads from the CRM and writes to the invoicing tool. Which permission system governs the agent? If each app has its own RBAC, the agent either gets full access to both (dangerous) or you build custom bridging logic (fragile).
Role explosion. The NIST literature flags role explosion as the primary scaling problem in RBAC. Per-app systems make it worse because the same logical role ("finance team member") gets duplicated and customized in each application. In a centralized system, you define it once.
One Hacker News commenter described this problem precisely: "Needed complex authentication, permissions, and global theming per client." Per-app RBAC cannot deliver "per client" when each app has its own permission universe.
RBAC vs. ACL vs. ABAC
RBAC is not the only access control model. Understanding the alternatives helps you pick the right one, and most production systems use a combination.
ACLs (Access Control Lists)
An ACL attaches permissions directly to a resource. File systems use ACLs: this file is readable by Alice, writable by Bob, executable by the ops group.
ACLs work well for flat, resource-level access. They break down when you have thousands of resources and hundreds of users, because you end up managing permissions per resource per user. There is no abstraction layer.
Use ACLs for: filesystem permissions, object storage policies, network rules. Do not use ACLs for: application-level permissions that need to scale with your team.
ABAC (Attribute-Based Access Control)
ABAC evaluates attributes on the user, the resource, the action, and the environment to make access decisions. "Allow access if the user's department is finance AND the document's classification is internal AND it is a business day."
ABAC is more expressive than RBAC. It handles context (time, location, device) that RBAC ignores. OWASP's Authorization Cheat Sheet recommends ABAC over RBAC for complex systems because it avoids role explosion.
But ABAC is harder to implement, harder to audit, and harder to reason about. Policies are rules, not a structure. When an auditor asks "what can this user do?", answering that question in an ABAC system requires evaluating every policy against every possible attribute combination.
Use ABAC for: fine-grained, context-dependent access decisions (e.g., "only allow access from the corporate network during business hours"). Do not use ABAC for: your primary permission model in internal tools. The overhead is not worth it unless you have regulatory requirements that demand context-aware access.
The practical answer
For internal tools, RBAC with namespaced permissions handles 95% of cases. Layer ABAC on top when you need context-aware rules (IP restrictions, time-based access, device policies). Most teams never need pure ABAC.
Five principles that matter
1. Least privilege
Every user and every agent gets the minimum permissions needed for their function. Not "start with everything and remove what they do not need." Start with nothing and add what they do need.
Jerome Saltzer formalized this in 1974: "Every program and every privileged user of the system should operate using the least amount of privilege necessary to complete the job."
In practice, this means your default role has zero permissions. A new team member gets a "viewer" role that allows read access to the apps they need. Elevated access requires explicit assignment.
2. Deny by default
OWASP lists this as a top authorization principle: applications must deny access unless an explicit permission exists. If a permission check fails, the answer is "no," not "let me check if there is a reason to deny." No ambiguity.
Sounds obvious. Most AI-generated apps do the opposite. Claude Code generates a CRUD app where every route is open by default, and you add protection later. The secure approach reverses this: lock every route, then explicitly open the ones that need to be public.
3. Centralize authorization
OWASP recommends using "framework-level mechanisms enabling global, application-wide authorization configuration rather than applying checks individually to methods or classes."
The same logic applies across apps. Five internal tools should share one authorization layer, not five. One set of roles, one set of permissions, one user directory.
Per-app authorization is a dead end. Every AI-coded app is an island, and per-app RBAC means island permissions. Centralize it or watch your permission systems drift apart.
4. Enforce server-side
OWASP is explicit: "Developers must never rely on client-side access control checks." Hiding a button in the UI is not security. A determined user (or a compromised agent) can call the API directly.
Enforcement must happen at the API layer. The frontend hides things the user cannot access (that is UX), but the server rejects unauthorized requests (that is security). Both layers are necessary. Only one is a security boundary.
5. Separation of duties
The person who submits an expense report should not also approve it. The agent that generates a financial transaction should not also execute it. This constraint prevents fraud and limits the blast radius of compromised accounts.
NIST's constrained RBAC model handles this with static separation (you cannot hold both roles) and dynamic separation (you cannot activate both roles in the same session). Sarbanes-Oxley and SOC 2 audits check for this.
RBAC for AI agents
Most platforms fall apart here. AI agents need the exact access control model as human users. Not a separate one, and not a bypass.
A senior engineer has admin access but uses it carefully. An AI agent with admin access has no judgment. A single prompt injection gives an attacker credential access, lateral movement, and impact across every resource the agent can reach.
As one Hacker News commenter recommended: "Constrain them like any other service with production access: least privilege, audit trails, blast radius containment."
In practice:
Agents get roles, not open access. An agent that reads CRM data and sends follow-up emails gets a role with app:crm:contacts.read and integration:email:send. Nothing else.
The audit trail logs agent actions alongside human actions. Not a separate log. The same immutable trail, with the same schema: who (the agent), what (the action), when, and on which record.
The API enforces permission boundaries. If the agent tries to delete a record it does not have permission to delete, the request fails. Same as it would for a human without that role.
How this works on RootCX
On RootCX, RBAC is built into the platform. Not per-app. Every app, agent, and integration on a project shares one permission model, one set of roles, and one user directory.
You declare your data. Permissions follow.
Every RootCX app has a manifest.json with a dataContract: your entities and fields. When the app is installed, the platform generates namespaced CRUD permissions for every entity:
app:crm:contacts.create
app:crm:contacts.read
app:crm:contacts.update
app:crm:contacts.delete
Add a deals entity, four more permissions appear. Add companies, four more. You never write permission logic. You never wire up middleware. RBAC follows from the data contract.
If you need permissions beyond CRUD (like contacts.export or reports.generate), you declare them in the manifest's permissions field. But most apps never need to. The auto-generated CRUD set covers the standard cases.
Wildcards keep role definitions clean: app:crm:* grants full CRM access, * grants global admin.
Roles with inheritance
Roles support inheritance. An "editor" inherits everything from "viewer" and adds write permissions. A "manager" inherits "editor" and adds delete. Change the viewer permissions, and editors and managers pick up the change automatically.
Role hierarchies are validated for cycles (up to 64 levels deep), so you cannot create an infinite inheritance loop.
One identity layer
If your team uses Okta, Microsoft Entra, Google Workspace, or Auth0, you configure OIDC once and every app inherits authentication automatically. New users get a default role on first login. Role claims from your identity provider map directly to RootCX roles.
One user directory. One role structure. Disable someone in your identity provider, and they lose access to everything on the project.
Enforcement at every layer
The API checks permissions on every request. No app:crm:contacts.delete permission, no delete. The frontend SDK hides UI elements the user cannot access, but the API is the real security boundary.
Agents under the same rules
AI agents on RootCX get scoped roles, not admin by default. The API enforces the agent's role on every action. If the agent reaches for a resource outside its permissions, the call is denied. Every action lands in the immutable audit trail alongside human actions.
No special bypass. No "agent mode" that skips checks.
The SSO tax
Most internal tool platforms gate RBAC and SSO behind enterprise pricing. Retool locks advanced permissions behind their Business plan at $50 per user per month. For a 30-person team, that is $18,000 a year for access controls that should be baseline security.
As one reviewer put it: "SSO and advanced permissions are table-stakes features that many organizations require for security compliance, and gating them behind a $50/user/month tier feels aggressive."
On RootCX, RBAC and SSO are included on every plan, including the free tier. Access control is not a premium feature. It is a security requirement.
The checklist before you ship
Before your internal tool goes to production:
- One identity source. Users log in through your identity provider (SSO/OIDC), not per-app passwords.
- Centralized roles. Roles and permissions are defined once and enforced across every app.
- Least privilege. New users and agents start with zero permissions and get only what they need.
- No default admin. The app does not ship with everyone as admin.
- Server-side enforcement. Every API endpoint checks permissions. The frontend hides but does not protect.
- Audit trail. Every action is logged with who, what, when, and on which record.
- Separation of duties. Critical workflows require more than one person.
- Agent boundaries. AI agents get scoped roles, not full access.
If you cannot check all eight, your tool is not ready for IT review.
From islands to infrastructure
What separates a working prototype from production software is not more features. It is infrastructure: authentication, access control, audit logging, session management. IT checks for these before approving a tool. Compliance audits for them when they show up.
Per-app RBAC is not a shortcut. It is technical debt that compounds with every new tool you build. Centralized RBAC, where every app and agent shares one role structure, one permission model, and one audit trail, is the only approach that scales.
On RootCX, this is built in from the first deploy. One identity layer, one permission model. Configure RBAC once, and every app you build after that inherits it.
Start your project. Free tier, no credit card required.