RBAC — Role-Based Access Control¶
OWNER: aimee ALSO_USED_BY: anna (implementation), hugo (auth enforcement), faye, sytske
Access control is scoped per-tenant. A user can be an Admin in Org A and a Viewer in Org B. Roles and permissions are always evaluated within the context of the current tenant.
Access Control Model Selection¶
SCOPE_ITEM: access_control_model
RBAC (Role-Based Access Control) — Default¶
INCLUDES: Users are assigned roles within an organization INCLUDES: Roles have a fixed set of permissions INCLUDES: Permission check: does user's role in this org allow this action on this resource? INCLUDES: Simple to understand, implement, and audit
When to use: 80% of B2B SaaS projects. Start here.
ABAC (Attribute-Based Access Control)¶
INCLUDES: Access decisions based on attributes (user attributes, resource attributes, environment) INCLUDES: Policy engine evaluates rules like: IF user.department = "finance" AND resource.type = "invoice" THEN allow INCLUDES: Fine-grained, field-level access control possible
When to use: Regulated industries, complex data sensitivity requirements, field-level permissions. CHECK: ABAC adds 60-100 hours — confirm requirement before including
ReBAC (Relationship-Based Access Control)¶
INCLUDES: Access based on relationships between entities (user → team → project → document) INCLUDES: Graph-based permission evaluation INCLUDES: "Can user X access document Y?" resolved by traversing the relationship graph INCLUDES: Inspired by Google Zanzibar (used by Google Drive, Notion, etc.)
When to use: Collaborative products with nested sharing, document hierarchies, team structures. CHECK: ReBAC adds 100-150 hours — only for products where sharing/collaboration is core CHECK: Consider OpenFGA or SpiceDB as external policy engines if ReBAC needed
Decision Matrix¶
IF: Simple team product with 3-5 predefined roles THEN: RBAC (default) IF: Product has field-level visibility requirements or conditional access rules THEN: RBAC + ABAC layer IF: Product has nested sharing, folder hierarchies, or Google-Drive-like permissions THEN: RBAC + ReBAC (or full ReBAC) IF: Client says "we need custom permissions for everything" THEN: Challenge scope — usually RBAC with custom roles is sufficient
Standard Role Hierarchy¶
SCOPE_ITEM: default_roles
| Role | Description | Typical Permissions |
|---|---|---|
| Owner | Org creator, cannot be removed | All permissions, transfer ownership, delete org |
| Admin | Full org management | Manage users, roles, billing, settings, all data |
| Member | Standard user | CRUD on assigned resources, view shared resources |
| Viewer | Read-only access | View resources, export data, no create/edit/delete |
| Billing | Billing-only access | Manage plan, payment methods, invoices. No data access |
| Guest | Limited external access | View specific shared resources only |
CHECK: Confirm role names with client — some prefer "Manager" instead of "Admin" CHECK: Owner role must have at least one user at all times — prevent orphaned orgs CHECK: Billing role is separate from Admin — finance team should not need data access
Permission System Design¶
SCOPE_ITEM: permission_system
Permission Structure¶
INCLUDES: Permission format: resource:action (e.g., projects:create, users:delete)
INCLUDES: Resources: users, projects, documents, invoices, settings, billing, integrations, audit_log
INCLUDES: Actions: create, read, update, delete, export, manage, invite
INCLUDES: Wildcard: resource:* (all actions on resource), *:* (super admin)
Permission Matrix (Example)¶
| Permission | Owner | Admin | Member | Viewer | Billing |
|---|---|---|---|---|---|
| users:invite | yes | yes | no | no | no |
| users:manage | yes | yes | no | no | no |
| projects:create | yes | yes | yes | no | no |
| projects:read | yes | yes | yes | yes | no |
| projects:update | yes | yes | own | no | no |
| projects:delete | yes | yes | no | no | no |
| billing:manage | yes | yes | no | no | yes |
| settings:manage | yes | yes | no | no | no |
| audit_log:read | yes | yes | no | no | no |
| audit_log:export | yes | no | no | no | no |
INCLUDES: "own" means user can only update resources they created CHECK: Generate permission matrix during scoping — client signs off
Custom Roles¶
SCOPE_ITEM: custom_roles INCLUDES: Org admin can create custom roles INCLUDES: Custom role = name + description + set of permissions (cherry-picked) INCLUDES: Custom roles scoped to organization (not global) INCLUDES: Maximum 20 custom roles per org (prevent permission sprawl) INCLUDES: Custom role assignment same as predefined roles OPTIONAL: Role templates (start from predefined role, modify permissions) CHECK: Custom roles add 30-50 hours of implementation CHECK: UI for permission selection must be clear — group permissions by resource
Organization Hierarchy¶
SCOPE_ITEM: org_hierarchy INCLUDES: Flat structure (single level) — default for most B2B SaaS OPTIONAL: Two-level hierarchy (org → teams/departments) OPTIONAL: Multi-level hierarchy (org → division → department → team) IF: Multi-level hierarchy needed THEN: Budget 60-100 extra hours, use RBAC with inheritance CHECK: Hierarchy adds complexity to every permission check — start flat CHECK: Most "hierarchy" needs are solved by teams (flat groups within org)
Team-Scoped Permissions¶
IF: Product has team concept THEN: Implement two-layer RBAC
INCLUDES: Org-level role (Admin, Member, Viewer) — controls org-wide actions
INCLUDES: Team-level role (Team Lead, Team Member, Team Viewer) — controls team data
INCLUDES: Permission check: org role AND team role evaluated together
INCLUDES: Team leads can manage team members but not org settings
Implementation with Drizzle (GE Stack)¶
Database Schema¶
SCOPE_ITEM: rbac_schema
INCLUDES: Table `roles` — id, tenant_id, name, slug, description, is_custom, is_default, created_at
INCLUDES: Table `permissions` — id, resource, action, description
INCLUDES: Table `role_permissions` — role_id, permission_id (many-to-many)
INCLUDES: Table `user_org_roles` — user_id, tenant_id, role_id (user's role in an org)
INCLUDES: Predefined roles seeded on tenant creation (Owner, Admin, Member, Viewer, Billing)
INCLUDES: Predefined permissions seeded on migration (all resource:action pairs)
Middleware Enforcement¶
SCOPE_ITEM: rbac_middleware
INCLUDES: Hono middleware: requirePermission('projects:create')
INCLUDES: Middleware reads user's role from JWT claims (cached, not DB lookup per request)
INCLUDES: Role's permissions loaded from Redis cache (invalidated on role change)
INCLUDES: If permission denied, return 403 with error code (not 404)
INCLUDES: Permission check happens AFTER tenant context is established
API Route Pattern¶
SCOPE_ITEM: rbac_route_pattern
INCLUDES: Every route declares required permission
INCLUDES: Route: POST /api/projects → requirePermission('projects:create')
INCLUDES: Route: GET /api/projects → requirePermission('projects:read')
INCLUDES: Route: PATCH /api/projects/:id → requirePermission('projects:update')
INCLUDES: Route: DELETE /api/projects/:id → requirePermission('projects:delete')
INCLUDES: Bulk operations check permission once, not per item
CHECK: Never rely on frontend to hide buttons as access control — always enforce server-side CHECK: 403 responses should include error code, not reason (information disclosure risk)
API Key Management¶
SCOPE_ITEM: api_key_management
INCLUDES: API keys scoped to organization
INCLUDES: Key format: prefix_randomstring (e.g., ge_live_abc123def456)
INCLUDES: Prefix indicates environment (live, test, sandbox)
INCLUDES: Key hashed with SHA-256 before storage (never store plaintext)
INCLUDES: Key displayed once at creation (full value), then only prefix shown
INCLUDES: Key has associated permission scopes (subset of role permissions)
INCLUDES: Key usage tracked (last used, total requests, created_by)
INCLUDES: Key rotation: create new key, grace period (48h), old key expires
INCLUDES: Maximum 10 API keys per org (prevent key sprawl)
COMPLIANCE: API key creation and deletion must be audit-logged
CHECK: API keys are NOT user tokens — they represent the org, not a person
CHECK: Rate limiting applies per API key, not per user
Scope-Based API Key Permissions¶
INCLUDES: Key scopes defined at creation: ["projects:read", "projects:create"]
INCLUDES: Scope validated on every API request using the key
INCLUDES: Narrower scope than the creating user's role is allowed
INCLUDES: Wider scope than the creating user's role is rejected
Personal Access Tokens¶
SCOPE_ITEM: personal_access_tokens INCLUDES: Per-user tokens (represent the user, not the org) INCLUDES: Inherit the user's current role permissions INCLUDES: If user's role changes, token permissions change too INCLUDES: Expiry: user-defined, max 1 year INCLUDES: Revocable by user or org admin OPTIONAL: Only if client's customers are developers (API-heavy product) CHECK: PATs simplify CLI and automation use cases — include if product has API focus
Platform Admin RBAC¶
SCOPE_ITEM: platform_admin_rbac INCLUDES: Separate from org-level RBAC — platform admins are the client's own staff INCLUDES: Roles: Super Admin, Support Agent, Billing Manager, Read-Only Auditor INCLUDES: Super Admin: all operations, impersonation, feature flags, system config INCLUDES: Support Agent: view org data, impersonate users, no billing or config changes INCLUDES: Billing Manager: manage plans, credits, invoices, no data access INCLUDES: Read-Only Auditor: view everything, change nothing INCLUDES: All platform admin actions written to separate audit log COMPLIANCE: Platform admin impersonation must be time-limited (max 1 hour) and audit-logged CHECK: Platform admin panel is accessed at /admin, not /settings — separate route group
Permission Caching Strategy¶
SCOPE_ITEM: permission_caching
INCLUDES: User's effective permissions cached in Redis on login
INCLUDES: Cache key: tenant:{tenant_id}:user:{user_id}:permissions
INCLUDES: Cache TTL: 5 minutes
INCLUDES: Cache invalidated on: role change, custom role permission change, user deactivation
INCLUDES: JWT contains role slug (not full permissions) — permissions resolved from cache
CHECK: Never put full permission list in JWT — token size bloats, stale permissions risk
CHECK: Cache miss falls back to database lookup (not denied)
Testing RBAC¶
SCOPE_ITEM: rbac_testing INCLUDES: Test: User with Member role cannot access Admin-only endpoints INCLUDES: Test: User with Viewer role cannot create, update, or delete resources INCLUDES: Test: Custom role with specific permissions grants exactly those permissions INCLUDES: Test: Role change takes effect within 5 minutes (cache invalidation) INCLUDES: Test: API key with limited scopes cannot exceed those scopes INCLUDES: Test: Deactivated user's sessions and API keys are immediately revoked INCLUDES: Test: Owner role cannot be removed from last remaining owner CHECK: RBAC tests run in CI on every deployment — permission bypass is severity-1