Feature role matrix
Which of the six roles can do what, one page per product seam. Open a section below, or read Roles first for what each persona is for.
Sections
Where the truth actually lives
Permissions are code, not documentation. Three files decide what a role can do, and the platform enforces from them directly:
| File | What it holds |
|---|---|
packages/services/src/permissions/registry.ts | The vocabulary — every <feature>.<action> the platform knows |
packages/services/src/permissions/defaults.ts | PLATFORM_ROLE_TEMPLATE_DEFAULTS — the permissions each role is seeded with |
apps/web/lib/route-policies.ts | Which roles may open which URL |
Server actions enforce the registry via requiredPermission, route handlers via
requirePermission, and the route proxy derives its allow-lists from the same defaults. There is
no separate list of who-can-do-what that the code consults — these pages describe that code, they
do not govern it.
For the live answer, read the scope and licences tab at /admin/settings/scope in the
platform (admin only). It renders every role’s effective capabilities and the surfaces each can
reach from the tenant’s own role templates and the route rules, so it reflects per-tenant edits
that these pages cannot show.
How these pages are maintained
Hand-maintained, one page per seam, and every row names the gate behind it — either the
permission that guards the action or the route rule that guards the page. That naming is the
maintenance story: change a holder in defaults.ts or a rule in route-policies.ts, and the rows
citing it are exactly the rows to update. The delivery pipeline’s Release stage does that in the same
PR as the change, via the docs-sync skill.
The choice was deliberate. There is no master file behind these pages any more: a
public/feature-role-matrix.md spreadsheet used to claim that title while documenting an
architecture the platform no longer has, and it contradicted these pages in roughly a dozen
places. It was retired rather than refreshed, and the per-section pages below stand alone.
Generating a static role-capability table from PLATFORM_ROLE_TEMPLATE_DEFAULTS at build time
remains an open option for later; nothing here depends on it landing.
Reading the tables
Each section splits into two tables:
- Capabilities — actions gated by a permission from the registry. Cells show the platform default for a fresh tenant; an admin who edits a role template changes their own tenant, not this page.
- Surfaces — pages, gated by
route-policies.ts. Most rules derive their allow-list from a permission; the rest carry an explicit role list and a stated reason, and those are literal — a role absent from an explicit list cannot open the page.
Cell conventions:
- ✔️ — held by default. ❌ — not held.
- own — the role reaches only its own records (the scope qualifier in
defaults.ts). - A parenthesised note such as (manager only) is a presentation qualifier the platform surfaces as a hint, not a second access level: the permission gate itself is binary.
adminis a hard superuser — the resolver short-circuits it to allow-all, so it holds every capability whatever the template says. It is not automatically on an explicit route list, which is why some Surfaces rows show ❌ for admin.