Staff roles by capability, not by name

Every vendor-side endpoint names the capability it needs, and roles are presets over those capabilities, so widening access is one visible table change.

What it is

Ten named capabilities and four roles. Admin runs the platform but never sees revenue. Support reads accounts, works bugs and can enter an account to help. Sales lives in the discovery panel and the signup queue and can enter an account of someone they just sold to. The owner passes every capability by identity rather than by enumeration.

Also called: support role · sales role · admin role · least privilege · role permissions for vendor staff

See it
Staff roles by capability, not by name
CustomerBuildingAmountStatus
Ivy BrubakerHobby Shop$15,830Open
Ronan PetschBarndominium$1,580Sent
Ronan PetschHobby Shop$15,830Paid
Ivy BrubakerHobby Shop$16,020Open
Gideon AltRiding Arena$17,540Sent
The role/capability grid from the code rendered as a table, with the role blurbs underneath. src/lib/platformAccess.ts. Sample data — no customer information appears here.
How it works
  1. 1Routes call requirePlatformCapability('tenants.manage') rather than checking a role or an identity.
  2. 2A denied request returns a 403 naming the missing capability, so the failure is diagnosable rather than a bare 'Forbidden'.
  3. 3The owner short-circuits: hasCapability returns true for the owner without consulting any list.
  4. 4The full capability list is enumerated with a compiler-enforced completeness check, so adding a capability without adding it there fails the build.
  5. 5Capabilities are shipped to the browser so the UI can hide what it must not offer — never as the basis of a server-side decision.
  6. 6Access is cached briefly, with a shorter window on 'no' so a freshly accepted invitation starts working within seconds.
Why we built it

The header explains why the model is capability-first: 'Before this, may this person administer the platform? was answered by ~40 route handlers each holding their own copy of the same email comparison. That is exactly the shape the July SEV-1 took: an authorization rule spread thin enough that widening it in one place looked local and turned out not to be.' The enumerated owner list has its own scar: it used to be derived as the union of the other roles' lists, and when one capability was removed from admin 'the derived union dropped it and the OWNER lost the revenue UI — narrowing a subordinate role quietly narrowed the top one.' Even the failure mode is a design choice: a database error 'must never be readable as a grant.'

The problem
  • The same authorization rule copied into dozens of endpoints drifts.
  • Role-name checks make it impossible to move one permission without touching every route.
  • A generic 'Forbidden' is indistinguishable from a bug.
Sound familiar?
What you get
Vendor staff get the narrowest access that lets them do their job.
Moving a permission between roles is one table edit, reviewable at a glance.
Denials say which capability was missing.

See it on your own jobs

Twenty minutes, your numbers, no slide deck. We’ll build one of your real buildings in front of you and send you the estimate link at the end — yours to keep either way.

or keep browsing features →