Skip to main content

ADR-0014 — Per-staff permission overrides

  • Status: Accepted
  • Date: 2026-05-06
  • Decision-makers: Tom Anderson

Context

The first implementation of staff permissions used a pure role-based model: role_permissions(role_id, screen_id, can_see). Toggling a tab visibility for one staff member changed it for everyone with the same role.

The shop owner pointed out that this is wrong: "When I check the tabs they can see, it sets that for every staff with that role." Real shops have staff who are nominally the same role but with different responsibilities. Junior A might handle returns; Junior B might not. They shouldn't have to be different roles for this to work.

Two ways to model this:

  • Roles + per-staff exceptions — keep role defaults, add an exception table that overrides for specific staff
  • Per-staff permissions only — give every staff a full permission set; no roles. Roles become onboarding presets.

The first is more flexible (a staff change doesn't lose their custom config; they pick up the new role's defaults for un-customized fields). The second is simpler but loses the "role-wide policy change" feature.

Decision

Implement role defaults + per-staff overrides:

  • roles(id, name) — Sys Admin, Owner, Sales, Mechanic, Junior, Service Lead, etc.
  • role_permissions(role_id, screen_id, can_see) — role defaults
  • staff_screen_permissions(staff_id, screen_id, can_see) — per-staff overrides; NULL or missing means "use role default"
  • Resolved permission: COALESCE(staff_override, role_default)

UI: the Staff & Permissions card shows the resolved view. Toggling a checkbox writes to staff_screen_permissions (per-staff), not role_permissions (per-role). An explicit "Reset to role defaults" button clears all override rows for that staff.

A separate UI (not yet built) for editing role defaults exists for when the owner wants to change defaults for everyone with that role.

Consequences

Positive:

  • Matches the operator's mental model: "I'm configuring this staff member's tabs"
  • Roles still exist for fast onboarding (new Junior gets sensible defaults)
  • Reset-to-defaults gives an out from per-staff customization gone weird
  • Audit log clearly shows whether a change affected one staff or a role

Negative:

  • Two tables to consult on every permission resolve
  • The "change a role default" path needs to handle the "what about staff with overrides" question — the answer is "their overrides win"; the role-default change only affects un-customized staff
  • More moving parts than pure-role

Mitigations:

  • The resolution is a single JOIN with COALESCE — cheap
  • Documented in the UI: each staff card shows which permissions are "role default" vs "overridden"
  • Owner can reset any individual staff to role defaults at any time

Notes

This decision was reactive — the first implementation got it wrong. Captured here because the wrong-way pattern is tempting and the right-way pattern needs to be remembered.

See also