Skip to content

Permissions

The SPA does not invent authorization. The Worker is the source of truth; the frontend mirrors that model for routing and UI so users do not see actions they cannot perform.

Related: Routing · Features · Lifecycle permissions matrix.


Three-layer model

LayerWhereJob
1. RoutePermissionGuard + routeConfigBlock whole pages
2. Action UIusePermissions() / hasPermissionHide or disable buttons
3. BackendWorker requirePermission*Final deny — always

If the UI is wrong, the API must still reject. If the API allows it, the UI should not invent a stricter rule without updating the shared permission catalog.


How the SPA gets permissions

  1. Auth / me load establishes the current user
  2. Permission IDs come from the user payload (and/or system catalog for labels)
  3. usePermissions() (apps/web/src/context/auth/usePermissions.ts) exposes helpers such as hasPermission(id)
  4. Super admin bypass is preserved in the client helper (aligned with Worker behavior)
  5. Role fallback is used only when a permission list is not present — prefer explicit IDs

System permission catalog (for admin UIs): systemServiceGET /system/permissions.


Route gating

  • Canonical map: apps/web/src/router/AppRoutes.tsx
  • Access table: apps/web/src/router/config/routeConfig.ts (getRouteAccess, ROUTE_ACCESS)
  • Wrapper: PermissionGuard applies access around the page element

Paths are not role-prefixed (/users, not /admin/users). Role folders under domains/* are for unique experiences, not for access control by path segment.

Missing page access → unauthorized / blocked route flow (not a blank screen).


Action gating

On shared pages (users, maintenance, departments, system):

typescript
const { hasPermission } = usePermissions();
// e.g. show Create only if hasPermission('user.create')
SituationUX
No page accessUnauthorized route
Page OK, action deniedHide or disable the control
Backend still deniesShow error from ApiError gracefully

Maintainer rules

  1. New routes must declare access in routeConfig and wrap with PermissionGuard.
  2. New destructive / privileged buttons must check a real permission ID from the Worker catalog.
  3. Do not hard-code role names for access when a permission ID exists.
  4. Keep lifecycle matrices in docs/lifecycle/ in sync when maintenance permissions change.