ADR-003: Custom Roles and Permissions
- Status: Accepted
- Date: 2026-04-16
- Author: CTO Agent (KALA-141)
Context
The original authorization model used a fixed Prisma Role enum:
admindept_headapproveremployee
That was enough for early API gates, but not enough for real companies. Tenants need roles like "Legal reviewer", "Finance operator", or "Regional manager" without waiting for a code deployment. At the same time, the product does not yet need a full permission-table many-to-many RBAC system with role hierarchy, resource-specific grants, and policy language.
Decision
Introduce SystemRole as the primary role model:
model SystemRole {
id String
name String
slug String
isSystem Boolean
allDepartments Boolean
departmentIds String[]
permissions String[]
}
Built-in roles are stored as SystemRole rows with isSystem: true.
Tenant-created roles use isSystem: false. User.roleId points at the role
row, while the legacy enum remains on User.role for compatibility and fallback
during migration.
Permissions are string keys from a central catalog. The wildcard * grants all
known permissions.
Alternatives Considered
Keep only the enum
Rejected. It would force every tenant-specific role into application code and would require deployments for policy changes.
Permission table many-to-many
Rejected for now. A normalized permission table with role-permission join rows is powerful, but it adds migrations, joins, UI complexity, and policy drift before the product has enough requirements to justify it.
JSON policy language
Rejected. A policy DSL is too flexible for the current admin UI and harder to audit than explicit permission keys.
Consequences
Positive
- Custom roles can be created and edited at runtime.
- Built-in roles stay protected through
isSystem. - Permission checks stay simple: string membership plus wildcard expansion.
- The legacy enum fallback keeps existing users and tests working during the migration.
Negative
- Permission keys must stay stable because they are persisted strings.
- Renaming a permission requires a data migration.
- There is no resource-level policy language; department scope covers the main isolation axis.
Implementation Notes
src/lib/permission-catalog.tsowns the valid key list.src/lib/permissions.tscomputes capabilities and effective permission sets.src/routes/roles.tsvalidates role CRUD and protects system roles.- Seed data creates the four built-in
SystemRolerows.