# 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.