02 — Permissions Matrix

A complete map of which role can do what, broken down by module. The grid below is the source of truth for "if I assign X to a user, what can they touch?"

How to read this doc

  • ✅ — the role grants this permission
  • ⬛ — the role does NOT grant this permission
  • The Super Admin column is always ✅; it's omitted from per-module tables to keep them readable. Super Admin holds every permission in the system.
  • Permission keys follow resource.action (e.g., asset.create).
  • The rightmost column shows whether a permission is flagged dangerous — the role editor renders dangerous permissions with a red badge so admins notice when they're handing one out.

Common reads — granted to every workflow role

Every workflow role inherits this set so that any logged-in field user can navigate, see master data, render hierarchy pickers, manage their own profile, and toggle theme/language.

Permission What it gates
asset.read View assets list and detail. Required by every workflow that references an asset.
organization.read / location.read / classification.read See the master-data hierarchy in pickers and labels.
hierarchy-config.read Read the configured level labels (Building / Floor / Room) so dropdowns can label themselves.
document.read / document.upload View and upload documents on entities the user has access to.
me.profile.read See own profile page.
me.notification.read See own notification inbox + bell.
me.notification-preference.update Manage own per-template channel preferences.
me.theme.update Change own theme (light / dark) and PrimeNG preset.
me.language.update Change own UI language.
global-search.use Use the top-bar search widget.

If you provision a strict-role user without these (e.g., for an API service account), the corresponding top-nav widgets and account-menu items are hidden, and the matching routes return 403.

Identity (Users / Roles / Permissions / Sessions / Login audit)

T.Req T.App T.Rec A.Plan Audit A.Rev Mobile C.Iss C.Ret A.Cust Dangerous
user.read ⬛ ⬛ ⬛ ✅ ⬛ ⬛ ⬛ ✅ ⬛ ⬛
user.create ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛
user.update ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛
user.delete ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ 🔴
user.reset-password ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ 🔴
user.impersonate ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ 🔴
user.export ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛
role.read ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛
role.create / .update / .assign / .export ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛
role.delete ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ 🔴
permission.read / .export ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛
permission.assign-direct ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ 🔴
login-audit.read ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛

Identity management is admin-only by design. Audit Planner and Checkout Issuer get user.read so they can pick from the user list (assigning an audit, choosing a custodian); the rest of identity stays with Super Admin.

Master Data (Organizations / Locations / Classifications / Vendors / Manufacturers)

T.Req T.App T.Rec A.Plan Audit A.Rev Mobile C.Iss C.Ret A.Cust Dangerous
organization.read ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅
organization.view-tree ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛
organization.create / .update / .export / .import ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛
organization.delete ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ 🔴
location.* (same shape as organization) reads ✅ for all roles, mutations admin-only
classification.* (same shape) reads ✅ for all roles, mutations admin-only
vendor.read / .create / .update / .export / .import ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛
vendor.delete ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ 🔴
manufacturer.read / .create / .update / .export / .import ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛
manufacturer.delete ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ 🔴

Master data is admin-curated. Workflow roles read but don't write. Vendor and Manufacturer read isn't included in commonReads — workflow users can still see them on the asset detail page (which loads them as part of the Asset record); the dedicated /vendors and /manufacturers pages are admin-only.

Assets

T.Req T.App T.Rec A.Plan Audit A.Rev Mobile C.Iss C.Ret A.Cust Dangerous
asset.read ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅
asset.create / .update / .export / .import ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛
asset.delete ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ 🔴
asset.print-label ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛

Asset CRUD is admin-only by default. Workflow flows mutate the asset indirectly — a transfer's Complete writes the new location, an audit's Approve writes back observed values. Direct edit lives at /assets/:id and needs asset.update.

Audits

T.Req T.App T.Rec A.Plan Audit A.Rev Mobile C.Iss C.Ret A.Cust Dangerous
audit-plan.read ⬛ ⬛ ⬛ ✅ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛
audit-plan.create ⬛ ⬛ ⬛ ✅ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛
audit-plan.update ⬛ ⬛ ⬛ ✅ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛
audit-plan.delete ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ 🔴
audit-plan.assign ⬛ ⬛ ⬛ ✅ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛
audit-plan.export ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛
audit-assignment.read ⬛ ⬛ ⬛ ✅ ✅ ✅ ✅ ⬛ ⬛ ⬛
audit-assignment.submit ⬛ ⬛ ⬛ ⬛ ✅ ⬛ ✅ ⬛ ⬛ ⬛
audit-result.read ⬛ ⬛ ⬛ ⬛ ✅ ✅ ✅ ⬛ ⬛ ⬛
audit-result.review ⬛ ⬛ ⬛ ⬛ ⬛ ✅ ⬛ ⬛ ⬛ ⬛

The four audit roles split the lifecycle cleanly: Audit Planner designs and assigns; Auditor / Mobile Auditor execute; Audit Reviewer reviews. None can do all three by default. A senior auditor commonly gets all four for self-service.

Transfers

T.Req T.App T.Rec A.Plan Audit A.Rev Mobile C.Iss C.Ret A.Cust Dangerous
asset-transfer.read ✅ ✅ ✅ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛
asset-transfer.create ✅ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛
asset-transfer.submit ✅ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛
asset-transfer.approve ⬛ ✅ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛
asset-transfer.reject ⬛ ✅ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛
asset-transfer.receive ⬛ ⬛ ✅ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛
asset-transfer.complete ⬛ ⬛ ✅ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛
asset-transfer.cancel ✅ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛
asset-transfer.export ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛
asset-transfer.delete ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ 🔴

The three transfer roles together cover the full Draft → Submit → Approve → InTransit → Receive → Complete chain. No single seeded role can take a transfer end-to-end by design — separation of duties.

Custody (Check-outs)

T.Req T.App T.Rec A.Plan Audit A.Rev Mobile C.Iss C.Ret A.Cust Dangerous
check-out.read ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ✅ ✅ ✅
check-out.create ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ✅ ⬛ ⬛
check-out.return ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ✅ ⬛
check-out.cancel ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ✅ ⬛ ⬛
check-out.export ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛

Custody splits cleanly: Issuer creates and cancels; Returner accepts; Custodian reads (their own list, scoped by mineOnly filter on the read endpoint).

Maintenance

T.Req T.App T.Rec A.Plan Audit A.Rev Mobile C.Iss C.Ret A.Cust Dangerous
maintenance-plan.read / .create / .update / .export / .generate ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛
maintenance-plan.delete ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ 🔴
maintenance-request.read / .create / .review / .export ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛
work-order.read / .create / .update / .assign / .close / .export ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛

Maintenance has no seeded workflow role. All maintenance permissions are held only by Super Admin out of the box. Real deployments typically create custom roles like:

  • Maintenance Planner — maintenance-plan.read/.create/.update/.generate (designs the recurring schedules)
  • Maintenance Reviewer — maintenance-request.read/.review + work-order.create/.assign (triages incoming requests, dispatches WOs)
  • Technician — work-order.read/.update/.close (executes assigned WOs and reports completion)
  • Reporter (often == every employee) — maintenance-request.create (file a new "broken X" request)

Create them at /roles/new and pick the matching permissions.

Permission split between Update and Close on Work Orders

Permission Gates these actions
work-order.update Start (Assigned → InProgress) and Cancel (any non-terminal status)
work-order.close Complete (InProgress → Completed) — captures cost, downtime, resolution notes

Splitting them lets organizations route the "kick off and abandon" path to the technician while reserving "officially complete with cost data" for a senior. Common bundling: same person gets both — no harm.

Notifications

T.Req T.App T.Rec A.Plan Audit A.Rev Mobile C.Iss C.Ret A.Cust Dangerous
notification-template.read / .update ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛
notification-template.delete ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ 🔴

The notification templates are admin-only. The "do I receive this notification?" toggle (per template, per channel) is a personal-account permission (me.notification-preference.update) and granted to every workflow role via commonReads.

Documents

T.Req T.App T.Rec A.Plan Audit A.Rev Mobile C.Iss C.Ret A.Cust Dangerous
document.read ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅
document.upload ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅
document.delete ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ 🔴

Workflow users can attach files (a damaged-asset photo, a signed transfer form, a vendor invoice) to entities they have access to. Deleting a document is admin-only.

Reporting (per-report keys)

T.Req T.App T.Rec A.Plan Audit A.Rev Mobile C.Iss C.Ret A.Cust Dangerous
report.asset-inventory.read / .export-excel / .export-pdf ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛
report.audit-results.read / .export-excel / .export-pdf ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛
report.audit-history.read / .export-excel / .export-pdf ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛
report.maintenance-history.read / .export-excel / .export-pdf ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛
report.transfer-history.read / .export-excel / .export-pdf ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛
report.checkout-activity.read / .export-excel / .export-pdf ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛

Reports are admin-only by default — none of the seeded workflow roles include them. Grant per-report so that, e.g., a Branch Manager sees only report.asset-inventory.read for their cost center, not the audit-history feed.

The split between .read, .export-excel, and .export-pdf lets you allow a manager to view a report in the UI but only export to one format — useful if PDF is the official archive and Excel exports are gated to a smaller group.

System

T.Req T.App T.Rec A.Plan Audit A.Rev Mobile C.Iss C.Ret A.Cust Dangerous
audit-log.read ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛

The audit log (every change to every business entity, with before/after JSON) is sensitive — Super Admin only by default. Compliance officers usually get this and nothing else.

Settings

T.Req T.App T.Rec A.Plan Audit A.Rev Mobile C.Iss C.Ret A.Cust Dangerous
hierarchy-config.read ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅
hierarchy-config.update ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ 🔴
app-settings.read / .update ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ .update is 🔴
app-language.manage ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ 🔴
translation.read / .update / .export ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ .update is 🔴
email-settings.manage ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ ⬛ 🔴

Workflow roles get only hierarchy-config.read (so dropdown labels render correctly). All other settings are admin-only.

Account (self-service)

T.Req T.App T.Rec A.Plan Audit A.Rev Mobile C.Iss C.Ret A.Cust Dangerous
me.profile.read ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅
me.notification.read ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅
me.notification-preference.update ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅
me.theme.update ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅
me.language.update ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅
global-search.use ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅

These are deliberately separated so an admin can provision a strict integration / service account without personal-UI affordances. For human users they're always granted.

How permissions resolve at runtime

Every API call goes through this:

flowchart LR
  REQ[Incoming request] --> A{Has access<br/>token?}
  A -- no --> REJ401[401 Unauthorized]
  A -- yes --> EFF[Compute effective<br/>permissions]
  EFF -- "(role permissions)<br/>∪ (direct grants)<br/>− (direct denies)" --> CHECK{Has the<br/>required permission?}
  CHECK -- yes --> OK[Action executes]
  CHECK -- no --> REJ403[403 Forbidden<br/>+ which permission was missing]

The result is cached for 30 minutes. When you change a user's roles, or change a role's permissions, the cache is invalidated immediately so the next request re-evaluates.

Building custom roles

Use the role designer at /roles/new (Super Admin only). The editor groups the permission catalog by module (Identity, Master Data, Assets, …) into collapsible cards. Search filters across name and key. A "Selected: X / Y" badge tracks total selection. A dirty indicator on the save bar tells you that you've changed something but haven't saved yet.

Tips when designing a new role:

  1. Always include the common reads. A role without them is a service-account role; humans need them to see the UI.
  2. Avoid mixing approve + create. A role that can both create and approve a transfer breaks separation of duties.
  3. Be explicit about export. A role with *.read doesn't automatically get *.export — that's intentional. Add it if you want analysts to download CSV.
  4. Dangerous permissions (the 🔴 ones) get a red badge in the role editor. Sleep on it before granting delete or update rights to users.

Per-user overrides

The role × permission grid is the primary mechanism, but the system also supports per-user overrides (UserPermission rows):

  • Grant — add a single permission to a single user, bypassing roles. Useful when one person on a team needs a special capability without creating a new role.
  • Deny — explicitly remove a permission from a user even if a role grants it. Deny always wins.

Manage these at /users/:id → "Direct permissions" tab. Both have time-bound ValidFrom / ValidUntil columns — you can grant a permission "until end of next month" automatically.

Where to go next

To learn… See
What role bundles work well in real organizations 01 §Combining roles
How each workflow flows end-to-end 03 — Business Processes
Which screens each permission unlocks 04 — Features by Module