Roles & Permissions: Build RBAC That Scales Past Three Roles Without Becoming a Mess
RBAC Strategy for Your New SaaS
Goal: Ship a roles-and-permissions system customers actually understand — Admin / Member / Viewer covers v1, custom roles come later only if buyers ask, every check is centralized in middleware, and the model can grow with the product without becoming a tangle of hardcoded if (user.email === ...) checks. Avoid the failure modes where founders sprinkle authorization checks across the codebase (one missed check = a security hole), build a "flexible" RBAC system before they have a real use case (over-engineered, hard to use), or invent role names that mean different things in different contexts.
Process: Follow this chat pattern with your AI coding tool such as Claude or v0.app. Pay attention to the notes in [brackets] and replace the bracketed text with your own content.
Timeframe: Three-role v1 (Admin / Member / Viewer) shipped in 1-2 days. Permission-check middleware + UI affordances in week 1. Custom roles + audit + invite flow in weeks 2-3. Quarterly model review baked in.
Why Most Founder RBAC Is Broken
Three failure modes hit founders the same way:
- Hardcoded checks scattered across the codebase. Founder writes
if (user.email === 'admin@…')in three places, thenif (user.role === 'admin')in five more, thenif (user.account.tier === 'enterprise')in the API layer. Adding a role means hunting through every file. Removing one is even worse. Inevitably a check is missed; a Viewer can do something they shouldn't; the customer notices before the founder does. - Premature flexibility. Founder reads about Casbin and Cedar and ships a 12-table policy engine before there's a single customer asking for custom roles. The system is "powerful" but nobody on the team can predict what permission grants what — including the founder, six months later.
- Misaligned mental models. Customer thinks "Manager" should be able to invite users. Founder thinks "Manager" should be able to view reports but not invite. The invite UI says one thing; the API enforces another. Trust erodes; support tickets pile up.
The version that works is structured: ship three roles in v1, centralize all permission checks in middleware, expose checks to the UI so users see-or-don't-see actions consistently, add custom roles only when a paying customer asks, and audit every permission change.
This guide assumes you have already done Authentication (RBAC presupposes auth), have shipped Multi-Tenant Data Isolation (roles are scoped per workspace/account), have considered SSO & Enterprise Auth (enterprise customers expect role mapping from their IdP), and have shipped Audit Logs (every role change should be logged).
1. Start With Three Roles, Not Twelve
The first decision is the v1 role catalog. Resist the urge to ship a flexible permission engine before you have customers who need it.
Help me design the v1 role catalog for [my product] at [your-domain.com].
The three-role baseline that works for ~80% of B2B SaaS:
**Admin**
- Can do everything within the workspace/account
- Manages billing, users, settings
- Can invite, remove, and change roles of other users
- Cannot do anything outside their own workspace (multi-tenancy boundary)
**Member**
- Can do the core "work" the product enables
- Cannot manage billing, users, or workspace settings
- Cannot delete the workspace
- Can view the same things they can edit
**Viewer**
- Read-only access to the core "work"
- Cannot create, edit, or delete anything
- Cannot manage users or settings
- Useful for stakeholders who need visibility without write access
**Critical design decisions for v1**:
1. **Who is the first user of a new workspace?** Default: they're Admin. The "owner" concept can come later (or never — many products don't need a separate owner role).
2. **Can a workspace have multiple Admins?** Default: yes. Single-Admin breaks when the Admin leaves the company. Multiple Admins is the safe default.
3. **What happens when the last Admin tries to leave?** Block the action with a clear error: "You're the only Admin. Promote another user to Admin first, or delete the workspace."
4. **Can Members invite other Members?** Default: NO for v1 — Admins invite. Many products eventually allow Members to invite Viewers; build that when asked.
5. **What about Viewer-only workspaces?** A workspace with no Admin is broken state — your invite flow shouldn''t allow it.
**Anti-patterns to avoid in v1**:
- Shipping "Owner" as separate from Admin (over-complicates; Admin = full control is simpler)
- Shipping "Guest" or "Limited Member" before a customer asks (premature complexity)
- Shipping per-resource permissions (Member can edit Project A but not Project B) — this is a v2+ feature
- Letting customers create custom role names in v1 — wait for paying-customer requests
**Roles that come later** (only when justified):
- **Billing Manager**: handles billing without other admin access (request from finance teams)
- **Workspace Manager**: settings without billing (mid-market request)
- **Custom Role Builder**: create roles with arbitrary permission sets (enterprise-tier ask)
For my product, ask:
- Are there resource-level access patterns (per-project, per-folder) that need consideration?
- Does my pricing model imply role-based pricing (e.g., Viewers free, Members paid)? If yes, the Viewer role becomes load-bearing.
- Do my early customers come from products with established role models (Slack, Figma, Linear)? Match terminology where possible.
Output:
1. The v1 role catalog (3 roles, plus Owner if your product needs it)
2. The permission matrix per role (next section)
3. The default role for new workspace creators
4. The "last admin" handling rule
5. The list of roles you''re explicitly NOT shipping in v1, with reason
The biggest unforced error: shipping a flexible policy engine instead of hardcoded roles. If you have 0 customers asking for custom roles, you don't need a policy engine. Hardcoded roles plus a clean middleware layer is enough until paying customers ask for more.
2. Build the Permission Matrix
A role is a label; permissions are what it actually grants. Map them explicitly.
Help me build the permission matrix for [my product].
The pattern:
Permissions are named verbs on resources: `[action]:[resource]`. Examples:
- `read:projects`
- `create:projects`
- `update:projects`
- `delete:projects`
- `invite:users`
- `manage:billing`
- `read:audit_logs`
- `manage:integrations`
- `delete:workspace`
For each role, define which permissions it grants:
**Admin** (full access — typically every permission):
- `*` — wildcard
- Or explicitly enumerated if you''d rather not have wildcards anywhere
**Member**:
- `read:projects`, `create:projects`, `update:projects`, `delete:projects`
- `read:users` (see teammates) but NOT `invite:users` or `manage:users`
- NOT `manage:billing`, `manage:integrations`, `delete:workspace`
**Viewer**:
- `read:projects`
- `read:users`
- NOTHING else
**Storage**:
```sql
CREATE TABLE workspace_members (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
workspace_id UUID NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
role TEXT NOT NULL CHECK (role IN ('admin', 'member', 'viewer')),
invited_by UUID REFERENCES users(id),
invited_at TIMESTAMP NOT NULL DEFAULT NOW(),
joined_at TIMESTAMP,
removed_at TIMESTAMP,
UNIQUE(workspace_id, user_id)
);
CREATE INDEX idx_workspace_members_user ON workspace_members(user_id) WHERE removed_at IS NULL;
CREATE INDEX idx_workspace_members_workspace ON workspace_members(workspace_id) WHERE removed_at IS NULL;
Note: in v1, the role-to-permission mapping is hardcoded in your auth library. No permissions table; no roles table. Just a switch statement / dictionary.
// auth/permissions.ts
const ROLE_PERMISSIONS: Record<Role, string[]> = {
admin: ['*'],
member: [
'read:projects', 'create:projects', 'update:projects', 'delete:projects',
'read:users', 'read:audit_logs',
],
viewer: ['read:projects', 'read:users'],
}
export function hasPermission(role: Role, permission: string): boolean {
const grants = ROLE_PERMISSIONS[role]
return grants.includes('*') || grants.includes(permission)
}
Critical implementation rules:
- One source of truth. The matrix lives in ONE file. Every check imports from there. No copy-paste of role logic.
- Permission names are verbs on nouns. Not
canEditProjects(boolean style); it''supdate:projects(verb-noun style). Easier to enumerate, easier to add resources. - Wildcard for Admin is OK.
*for Admin saves listing every permission. Just be explicit that Admin has wildcard in the docs. - Per-resource permissions come later. "Member can edit Project X but not Project Y" is v2+. Don''t add this complexity until customers pay for it.
- Document the matrix. Customers (and your team) need to see what each role can do. Publish the matrix in your docs.
Don''t:
- Use boolean columns on the user (
is_admin,can_invite) — accumulates over time - Conflate roles and tiers (a Pro-tier Member is still a Member; the role-permission mapping is the same)
- Allow per-environment role overrides ("Admin in dev, Viewer in prod") — confuses everyone
Output:
- The permission catalog (every verb:noun your product cares about)
- The role → permissions mapping in one file
- The
hasPermission(role, permission)helper - The customer-facing permission matrix in your docs
The single most important piece of code: **`hasPermission(role, permission)` as a single function in a single file.** Everything else — middleware, UI checks, server checks — calls into this one function. When you change the role model, you change one file.
---
## 3. Centralize Checks in Middleware
Authorization spread across the codebase = inevitable holes. Centralize.
Help me design the authorization middleware.
The pattern:
Server-side (every protected route runs through middleware):
// middleware/authorize.ts
export function requirePermission(permission: string) {
return async (req, res, next) => {
const { user, workspaceId } = req
if (!user || !workspaceId) return res.status(401).end()
const member = await getWorkspaceMember(workspaceId, user.id)
if (!member) return res.status(403).json({ error: 'not_a_member' })
if (!hasPermission(member.role, permission)) {
return res.status(403).json({ error: 'permission_denied', required: permission })
}
req.member = member
next()
}
}
// Usage in route handlers:
router.delete('/projects/:id', requirePermission('delete:projects'), handler)
router.post('/users/invite', requirePermission('invite:users'), handler)
router.put('/billing', requirePermission('manage:billing'), handler)
Server actions / RPC (e.g., Next.js server actions):
export async function deleteProject(projectId: string) {
const member = await requireWorkspaceMember(currentWorkspaceId)
requirePermission(member.role, 'delete:projects')
// ...
}
Critical rules:
- Default-deny. Routes without
requirePermission(...)should not exist. Or if they do, they''re obviously public (/login,/signup,/healthz). - Workspace context comes from the route, not the user. A user with admin in Workspace A and member in Workspace B has different permissions per workspace. Look up the membership for the workspace this request is acting on.
- The denial response is consistent. 403 with
{ error: 'permission_denied', required: 'delete:projects' }lets your frontend show a useful error message. - Performance: cache the membership lookup per request (you''ll hit it many times in one request handler). Don''t cache across requests — role changes need to take effect immediately.
- Unit tests for the middleware itself + property-based tests that every protected route calls
requirePermission.
Linting rule (highly recommended):
- Write a custom ESLint rule (or use a regex-based pre-commit hook) that fails CI if a file in
routes/doesn''t importrequirePermissionsomewhere. Forces the discipline.
Don''t:
- Write
if (user.role === 'admin')outside this middleware. Ever. - Pass roles into business logic (
createProject(role, data)) — business logic shouldn''t care about roles. - Override authorization for internal admin tools without a SEPARATE auth path (per Internal Admin Tools)
Output:
- The authorization middleware code
- The list of every protected route with its required permission
- The linting / pre-commit rule to enforce centralization
- The unit tests covering the matrix
The single most important architectural decision: **never write authorization logic outside the middleware.** Once `if (user.role === ...)` appears in a route handler, the system rots fast. Pre-commit hooks or lint rules are how you enforce this past the first 10 routes.
---
## 4. Mirror Permissions in the UI
Server checks prevent damage. UI affordances prevent confusion. Both matter.
Design the UI affordances for permissions.
The pattern:
On the client, expose a useCan() hook:
const can = useCan() // pulls current user role from session/context
return (
<>
{can('delete:projects') && <DeleteButton />}
{!can('manage:billing') && <p>Contact your admin to update billing.</p>}
</>
)
Critical UI rules:
- Hide actions the user can''t perform. Don''t show a "Delete" button to a Viewer. Hiding > disabling > showing-but-failing.
- Disable with a tooltip when context matters. If the action exists conditionally ("Delete is hidden because this project has 12 collaborators"), use a disabled button with hover text explaining why.
- Empty states change per role. A Viewer on the Settings page sees "Settings are managed by your workspace admin"; an Admin sees the full settings UI.
- 403 error from the server should fall back to a useful message, not a blank page. The error contains
required: 'delete:projects'; map that to UI copy: "You don''t have permission to delete projects. Ask your workspace admin." - The role name shows up in the UI. A user should see "You''re a Member of Acme Corp" somewhere prominent (settings page, workspace switcher).
Anti-patterns:
- Showing the action and failing on click (frustrating)
- Hiding actions without explanation when context matters (confusing)
- Different role names in UI vs API (Admin in UI, "owner" in code) — pick one
The "request access" pattern (highly recommended):
- When a user tries to do something they can''t, surface a "Request access" affordance
- Click → sends a notification to all Admins of the workspace: "[user] requested permission to [action]"
- Reduces support tickets ("how do I change my role?") and creates a paper trail
Output:
- The
useCan()hook - The UI components updated to gate actions
- The empty-state copy per role
- The 403 error fallback component
- The "request access" flow (optional but recommended)
The biggest UX win: **showing a Viewer the same data with affordances disabled, rather than hiding the data.** A Viewer who sees the dashboard with "Edit" buttons greyed out understands their role. A Viewer who sees a stripped-down version of the dashboard wonders if data is missing.
---
## 5. Build the Invite and Role-Change Flows
Customers spend more time managing roles than initially picking them. The flows have to be smooth.
Design the invite and role-change UI.
Invite flow:
- Admin goes to /workspace/members
- Clicks "Invite member"
- Modal: email + role selector (default: Member)
- Submit → email sent with magic-link to accept invite
- Recipient lands on /accept-invite?token=…, signs in or signs up
- On accept:
workspace_membersrow created;joined_atset - Success message; redirect to workspace dashboard
Role-change flow:
- Admin goes to /workspace/members
- Sees a table of members with role dropdown next to each
- Changes a role → confirm dialog ("Change [user]''s role from Member to Admin?")
- On confirm: update workspace_members row; log to audit
- Send notification email to the affected user: "Your role in [workspace] was changed to [new role] by [admin]"
- Their next request reflects the new role (no need to re-login if you cache appropriately)
Member removal flow:
- Admin clicks "Remove" next to a member
- Confirm dialog ("Remove [user] from [workspace]? They will lose access immediately. Their data contributions are preserved.")
- On confirm: set
removed_at = NOW(); log to audit - Email the affected user: "You were removed from [workspace] by [admin]"
- Their session in that workspace is invalidated; they get a clean 403 on next request
Critical rules:
- Last-admin protection. Block the UI from removing or downgrading the last Admin. Show "[user] is the only Admin. Promote another user to Admin first."
- Self-removal. Allow a user to leave a workspace they''re in (NOT the same as Admin removing them). Block if they''re the only Admin.
- Confirm role escalations. Going from Viewer → Admin should require a confirm; the reverse can too.
- Audit every change. Per Audit Logs: role changes are high-value audit events.
- Invitations expire. A pending invite older than 7 days should auto-expire (with re-send option).
- Email is the source of truth, not user_id. Inviting
alice@acme.comshould work whether or not Alice already has an account. The invite resolves to a user on accept.
Don''t:
- Allow inviting Admins to be a casual checkbox — make it explicit
- Allow bulk role changes without per-user confirmation in v1 (ships easier; bulk comes later)
- Send only one notification (email) — also show the change in-app
Output:
- The invite UI + email template
- The role-change UI + confirmation dialogs
- The member-removal flow
- The last-admin protection logic
- The expired-invite handling
The single most-overlooked detail: **last-admin protection.** Without it, the founder demos the product, demotes themselves to Member to "see what it''s like," and locks themselves out. Trust me on this one.
---
## 6. Audit Every Permission Change
Without audit, "who removed Alice?" becomes a Slack archaeology project.
Design the audit log integration for RBAC.
Per Audit Logs, log these events:
Membership events:
workspace.member.invited— who invited, who, what roleworkspace.member.joined— when they acceptedworkspace.member.role_changed— old role, new role, who changedworkspace.member.removed— who removed, who was removedworkspace.member.left— self-removal
Permission-denied events (high-volume; sample, don''t log everything):
- DO log: explicit unauthorized attempts at sensitive actions (delete:workspace, manage:billing)
- DON''T log: every
read:*denial (too noisy) - Sample at 1-10% for less-sensitive denials
The audit entry shape:
{
"id": "uuid",
"workspace_id": "uuid",
"actor_id": "uuid", // who did it
"actor_role": "admin", // their role at the time
"event_type": "workspace.member.role_changed",
"subject_id": "uuid", // who/what was affected
"metadata": {
"old_role": "member",
"new_role": "admin"
},
"ip": "1.2.3.4",
"user_agent": "...",
"created_at": "2026-04-29T..."
}
Customer-facing audit feed at /workspace/audit:
- Filterable by event type, actor, date range
- Visible to Admins by default
- Optional: visible to Members for "their own" events (transparency)
- Exportable to CSV for compliance reviews
Don''t:
- Log raw passwords or tokens in audit metadata
- Allow audit logs to be edited or deleted (append-only)
- Forget to log self-actions (Alice removed herself is an event too)
Output:
- The audit event schema
- The event-emission code in the membership/role middleware
- The customer-facing audit feed
- The retention policy (typically 1-7 years for compliance)
The single most useful audit query in practice: **"who changed [user]''s role in the last 30 days?"** When something feels wrong, this is the first question. Make sure your audit feed answers it in under 10 seconds.
---
## 7. SSO and IdP Role Mapping (Phase 2)
Enterprise customers expect their IdP (Okta, Microsoft Entra, JumpCloud, Google Workspace) to drive role assignment. Build it when the first enterprise prospect asks.
Design SSO role mapping per SSO & Enterprise Auth.
The pattern:
When a user signs in via SSO:
- Their IdP sends a SAML/OIDC assertion containing their group memberships
- Your auth handler reads the groups from the assertion
- You map IdP groups → your roles via a per-customer config
Storage:
CREATE TABLE workspace_sso_role_mappings (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
workspace_id UUID NOT NULL REFERENCES workspaces(id),
idp_group_name TEXT NOT NULL, -- e.g., "engineering-leads"
role TEXT NOT NULL, -- e.g., "admin"
precedence INT NOT NULL DEFAULT 0, -- if multiple groups map, highest precedence wins
UNIQUE(workspace_id, idp_group_name)
);
On SSO login:
- Resolve the user
- Look up
workspace_sso_role_mappings WHERE idp_group_name IN (assertion.groups) - If found: set the user''s role to the highest-precedence match
- If no mapping: fall back to the user''s manually-assigned role (or a default)
SCIM provisioning (enterprise tier):
- Customer''s IdP (Okta, Entra) pushes user create/update/delete to your SCIM endpoint
- Each operation maps to: invite, role-change, remove
- Eliminates manual user management for enterprise customers
- Implement against the SCIM 2.0 spec
Critical rules:
- The mapping is per-workspace. Different customers map their IdP groups differently.
- IdP-driven roles override manual assignment for SSO users. If the customer wants manual override, they take the user out of the IdP group.
- Provide a UI for the customer to configure mappings. Don''t make them ask support.
- Test the mapping with a real IdP before enterprise sales calls. Most enterprise procurement asks for a test login.
Don''t:
- Hardcode group names — make them per-workspace configurable
- Block login when no mapping exists — fall back to a default role
- Skip SCIM if customer asks; it''s the price of enterprise
Output:
- The IdP group → role mapping table
- The SSO callback that applies mappings
- The customer-facing mapping configuration UI
- The SCIM 2.0 endpoint (if applicable)
---
## 8. Handle the Edge Cases
The role model gets stress-tested by edge cases. Plan for them before they happen.
Walk through the edge cases.
Edge case 1: User in multiple workspaces with different roles
- Alice is Admin of Acme, Member of Beta, Viewer of Gamma
- The role applies per-workspace; the workspace context comes from the request
- The UI should clearly show which workspace the user is "in" right now
- Workspace switcher should reflect their role in each
Edge case 2: Account ownership transfer
- Founder leaves the company; Admin role needs to transfer
- Pattern: "Transfer Ownership" button, single Admin-to-Admin transfer with email confirmation
- After transfer: the original Admin is downgraded to Member (or removed, with confirm)
Edge case 3: Workspace deletion
- Deleting a workspace cascades to all memberships
- Require Admin role + secondary confirmation (type the workspace name)
- Soft-delete with 30-day recovery window for paying customers
Edge case 4: Last user leaves the workspace
- If the last Admin leaves: workspace becomes orphaned
- Pattern: block self-removal of the last Admin; force them to delete the workspace if no other Admins
- Or: allow self-removal but mark workspace as "needs new admin" with a reclaim flow (more complex)
Edge case 5: Suspended user
- User violates terms or churns; admin wants them out without deleting their data contributions
- Pattern:
suspended_atcolumn onworkspace_members; suspended users get 403 on any request; their authored data is preserved - Restore by clearing
suspended_at
Edge case 6: Service accounts / bot users
- For automation or integrations: create users that don''t correspond to humans
- Pattern: a
is_botflag on the user, with a managed authentication path (typically API keys per API Keys) - Bots can be assigned roles same as humans
Edge case 7: External collaborators (Guest)
- Customer wants to invite an outsider with limited access
- v1: stretch the Viewer role to cover this
- v2+: a real "Guest" role with per-resource scoping (advanced)
Output:
- The handling for each edge case
- The UI affordances for ownership transfer and suspension
- The bot/service-account flag and management UI
---
## 9. When to Add Custom Roles
Custom roles ("create your own role with these permissions") sound great in a sales call and become a maintenance nightmare. Defer until you can''t.
The decision criterion for shipping custom roles.
Add custom roles when:
- A paying customer at >$1K MRR has explicitly asked
- The sales team has lost or is at risk of losing a deal because of the limitation
- Your existing 3-5 baked-in roles can''t reasonably model a customer''s structure
Don''t add custom roles when:
- A free-tier user is asking
- It''s a "nice to have" with no specific use case
- The team is bored
When you do add them, the v1 of custom roles:
- Customer creates a named role ("Auditor", "Read-Only Plus")
- They pick from your existing permission catalog (per the matrix from step 2)
- Roles are scoped per workspace — Acme''s "Auditor" is separate from Beta''s "Auditor"
- A handful of "system" roles cannot be edited or deleted (Admin, Member, Viewer)
Storage:
CREATE TABLE workspace_roles (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
workspace_id UUID NOT NULL REFERENCES workspaces(id),
name TEXT NOT NULL,
description TEXT,
permissions TEXT[] NOT NULL,
is_system BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
UNIQUE(workspace_id, name)
);
The workspace_members.role column changes from TEXT to UUID REFERENCES workspace_roles(id).
Critical rules:
- Don''t let customers grant permissions you haven''t implemented. The picker shows only permissions in your catalog.
- Limit the number of custom roles. 10 per workspace is plenty in v1.
- Custom roles should be visible to all members in some "Roles & Permissions" page so people understand what their role grants.
- Cap blast radius. Custom roles can''t grant permissions to delete the workspace, transfer ownership, or change billing — those stay Admin-only.
Output:
- The decision criterion documented in writing
- The v1 spec for custom roles when you ship them
- The migration from baked-in to custom roles
- The "system roles can''t be deleted" guardrail
The single biggest mistake at this stage: **shipping custom roles before having a paying customer ask.** The complexity multiplies your test surface, your support load, and your security review. Earn the complexity by having a customer pay for it.
---
## 10. Quarterly Review
The role model rots. Quarterly review keeps it healthy.
The quarterly review checklist.
Health metrics:
- What % of workspaces use only the baked-in roles? (Custom roles should serve real needs, not be a default.)
- What % of workspaces have 1 Admin only? (Single-Admin workspaces are bus-factor risks.)
- What % of permission-denied events are at the API layer vs UI? (UI denials should massively outnumber API ones; API denials are red flags — UI affordances are missing.)
- Are there permissions in the catalog that nothing checks? (Dead permissions; remove.)
- Are there checks in the codebase that bypass
hasPermission()(per the lint rule)?
Catalog hygiene:
- New product features added this quarter — were the permissions added consistently?
- Any role that''s mostly a duplicate of another? Consolidate.
- Customer-requested permissions added since last review — are they still needed?
Customer-impact review:
- Top 5 support tickets about roles/permissions — what pattern do they share?
- Any churn citing role/permission limitations?
- Sales objections related to RBAC?
Output:
- Health snapshot
- 3 fixes to ship next quarter
- 1 permission to deprecate
- 1 customer feedback theme to prioritize
---
## What "Done" Looks Like
A working RBAC system in 2026 has:
- **Three system roles** (Admin / Member / Viewer) covering 80% of use cases out of the box
- **A single `hasPermission()` function** that all checks call into
- **Authorization middleware** on every protected route, enforced by lint rule
- **UI affordances** mirrored to permissions via `useCan()`
- **Last-Admin protection** built into the membership UI
- **Audit logs** for every membership and role change
- **Invite, role-change, and removal flows** with confirmations and notifications
- **SSO/IdP role mapping** when an enterprise customer asks
- **Custom roles** only when a paying customer pays for them
- **Quarterly model review** baked into the team rhythm
The system you build in week 1 will look broken by year 2 if you don''t review it. Customers'' security teams will ask "can a Viewer see audit logs?" during enterprise sales — being able to point them to a published permission matrix that says "no" closes deals. Being unable to answer or pointing at scattered code loses them.
---
## See Also
- [SSO & Enterprise Auth](sso-enterprise-auth-chat.md) — IdP integration drives role mapping for enterprise customers
- [Multi-Tenant Data Isolation](multi-tenancy-chat.md) — roles are scoped per workspace; multi-tenancy is the boundary
- [API Keys & PATs](api-keys-chat.md) — keys carry scopes that interact with role permissions
- [Audit Logs](audit-logs-chat.md) — every role change logged
- [Internal Admin Tools](internal-admin-tools-chat.md) — your own admin tools should NOT use customer-facing RBAC
- [Onboarding Email Sequence](onboarding-email-sequence-chat.md) — invite emails are part of the role flow
- [Authentication](https://www.vibereference.com/auth-and-payments/authentication) — RBAC presupposes auth
- [Auth Providers](https://www.vibereference.com/auth-and-payments/auth-providers) — Clerk, Supabase, Better Auth often handle the membership table
[⬅️ Growth Overview](README.md)