1.5 KiB
ADR-0007: RBAC permissions instead of role checks
- Status: Accepted
- Date: 2026-08-11
Context
if (user.role === 'ADMIN') spreads. Six months later authorization logic is scattered
across dozens of files, no one can answer "who can refund an order?" without grepping, and
adding a "Warehouse Supervisor" role means editing and redeploying application code.
Decision
Authorization is expressed only as permissions (product.update, order.refund),
declared on routes with @RequirePermissions(...) and evaluated by a global PermissionsGuard.
Permissions are code: the catalog in @sport/types is the source of truth, and the seed
reconciles the database against it. Roles are data: rows in roles/role_permissions that a
SUPER_ADMIN edits at runtime with no deploy.
The admin sidebar is built from the same catalog, so a user never sees a link to a screen they cannot use — presentation only; the API re-checks every request.
Consequences
Every authorization rule is one greppable decorator. New roles need no code. The permission set travels inside the access token, so guards do no database work on the hot path — which is precisely why access tokens are short-lived (ADR-0008): a revoked permission takes at most one token lifetime to take effect.
Alternatives considered
Role checks in code — rejected above. Full ABAC/policy engine — rejected as premature: nothing yet needs "can edit orders from their own store only". The permission model can grow into that if a real requirement appears.