RBAC Matrix
Role hierarchy (tenant-scoped): owner > admin > manager > employee > viewer
Platform Admin is a completely separate access system based on
platformAdmin/hipaaAdminboolean fields on the Better Auth user record (Postgres) and is not part of this matrix. See Platform Admin section below.
Incident Management
| Action | Viewer | Employee | Manager | Admin | Owner |
|---|---|---|---|---|---|
| Report incident | Yes | Yes | Yes | Yes | Yes |
| View public incidents | Yes | Yes | Yes | Yes | Yes |
| View private incidents | — | — | Yes | Yes | Yes |
| Add comment | Yes | Yes | Yes | Yes | Yes |
| Change status ³ | — | Yes | Yes | Yes | Yes |
| Assign incident | — | Yes | Yes | Yes | Yes |
| Resolve incident | — | Yes | Yes | Yes | Yes |
| Close incident | — | Yes | Yes | Yes | Yes |
Private incidents are flagged at creation. Only Manager and above can see them.
³ Viewer incident updates: Viewers hold the
UPDATE_INCIDENTserver permission but with an important restriction: they may only update incidents they reported themselves. Status transitions (change status, assign, resolve, close) remain restricted to Employee and above. TheUPDATE_INCIDENTpermission for Viewers is intentional and enforced at the action layer.
Policy Management
| Action | Viewer | Employee | Manager | Admin | Owner |
|---|---|---|---|---|---|
| View published policies (Company Documents) | Yes | Yes | Yes | Yes | Yes |
| Access Policy Management (/policies) | — | — | Yes | Yes | Yes |
| Create policy (blank or from template) | — | — | Yes | Yes | Yes |
| Edit draft policy content | — | — | Yes | Yes | Yes |
| Submit policy for approval | — | — | Yes | Yes | Yes |
| View approval thread | — | — | Yes | Yes | Yes |
| Comment on approval thread | — | — | Yes | Yes | Yes |
| Approve / reject policy | — | — | — ¹ | Yes ¹ | Yes ¹ |
| Request changes (revert approved → draft) | — | — | — | Yes | Yes |
| Publish policy | — | — | — | Yes | Yes |
| Acknowledge policy (mark as read) | — | Yes | Yes | Yes | Yes |
| Accept template update | — | — | Yes | Yes | Yes |
| Dismiss template update | — | — | Yes | Yes | Yes |
| View template update diff | — | — | Yes | Yes | Yes |
| Use Smart Template Discovery | — | — | Yes | Yes | Yes |
¹ Self-approval guard: the user who submitted a policy for approval cannot approve or reject their own submission, regardless of role. A second admin or owner must act on it.
Policy lifecycle states
draft → pending_approval → approved → published → expiring → expired
↑ ↓
(request changes) (rejected → draft)
- draft — editable by its manager/admin/owner creator; not visible to other roles.
- pending_approval — locked for editing; awaiting admin/owner decision.
- approved — acknowledged by approver; ready to publish.
- published — visible to all roles; employees are prompted to acknowledge.
- expiring — within 30 days of expiry; displayed with a warning badge.
- expired — past expiry date; renewal required.
Template linkage
When a policy is created from a master template, it stores sourceTemplateId and sourceVersion. If the platform team publishes a new template version, managers and above see a banner offering to Accept (merge updated sections) or Dismiss the update.
Risk Assessment
| Action | Viewer | Employee | Manager | Admin | Owner |
|---|---|---|---|---|---|
| Run assessments | — | — | Configurable ² | Configurable ² | Yes |
| View published results | Yes | Yes | Yes | Yes | Yes |
² Configurable access: Owner can grant or revoke Manager and Admin access via Module Management → Role Access. Default is on for both.
HIPAA Assessment
| Action | Viewer | Employee | Manager | Admin | Owner |
|---|---|---|---|---|---|
| Run HIPAA assessment | — | Yes | Yes | Yes | Yes |
Asset Management
| Action | Viewer | Employee | Manager | Admin | Owner |
|---|---|---|---|---|---|
| View assets | Yes | Yes | Yes | Yes | Yes |
| Create asset | — | Yes (pending) | Yes | Yes | Yes |
| Edit asset | — | Yes | Yes | Yes | Yes |
| Delete asset | — | Yes (pending) | Yes | Yes | Yes |
| Approve pending assets | — | — | Yes | Yes | Yes |
All create, edit, and delete actions require a justification (minimum 10 characters). This is stored per-action as
createJustification,editJustification, ordeleteJustificationon the asset record.Employee-created assets (customers and suppliers) are placed in
pendingstatus and require manager, admin, or owner approval before becoming active.Employee deletion requests set
status: 'pending_deletion'rather than immediately retiring the asset. A manager, admin, or owner must approve the deletion request before the asset is retired.Employees can edit existing assets directly (no approval required), but a justification is always recorded.
Employee Profile Updates
| Action | Viewer | Employee | Manager | Admin | Owner |
|---|---|---|---|---|---|
| Request phone/address update (own record only) | — | Yes | Yes | Yes | Yes |
| View approval requests | — | — | Yes | Yes | Yes |
| Approve / reject update requests | — | — | Yes | Yes | Yes |
| View another employee's address | — | — | — | Yes | Yes |
Employees may request updates to their own phone number and address only. All three approver roles (Manager, Admin, Owner) receive an
approval_requirednotification simultaneously. Only one approval is needed to process the change. Once acted upon, all pending approval notifications are removed from every approver's inbox. The requester is notified of the outcome via the in-app notification bell.
Module Management
| Action | Viewer | Employee | Manager | Admin | Owner |
|---|---|---|---|---|---|
| Access Module Management | — | — | — | — | Yes |
| Enable / disable modules | — | — | — | — | Yes |
| Grant/revoke Admin role access | — | — | — | — | Yes |
| Grant/revoke Manager role access | — | — | — | — | Yes |
Changes apply to all users of the affected role within the tenant.
Company Settings
| Action | Viewer | Employee | Manager | Admin | Owner |
|---|---|---|---|---|---|
| Edit company profile | — | — | — | Yes | Yes |
| Publish policy statements | — | — | — | Yes | Yes |
| Publish risk assessment | — | — | — | Yes | Yes |
| Manage modules | — | — | — | — | Yes |
Platform Admin Portal
The Platform Admin Portal is completely isolated from the tenant RBAC system. A user with the owner role inside any tenant has no access here, and a platform admin has no access to tenant data.
Access is granted by setting platformAdmin: true or hipaaAdmin: true on the Better Auth user record in Postgres, using the pnpm set:admin <email> script (see PLATFORM_ADMIN.md).
| Portal | Better Auth field required | Entry point | Areas |
|---|---|---|---|
| Policy Admin | platformAdmin: true |
/platform-admin hub |
Policy Templates, Template Rules |
| HIPAA Admin | hipaaAdmin: true |
/platform-admin hub → /hipaa-admin |
Companies, Participants, Reports |
A single account can hold both claims. The set:admin script grants all claims at once.
Platform Admin capabilities
| Capability | Tenant Admin/Owner | Platform Admin (platform_admin) | HIPAA Admin (hipaa_admin) |
|---|---|---|---|
| Manage tenant's own policies | Yes | — | — |
| Approve / reject tenant policies | Yes | — | — |
| Author master policy templates (all tenants) | — | Yes | — |
| Publish template versions + notify tenants | — | Yes | — |
| Define template recommendation rules | — | Yes | — |
| Manage HIPAA companies and participants | — | — | Yes |
| View HIPAA assessment reports | — | — | Yes |
Enforcement
The field is verified independently at three layers — no single bypass point:
- Login page (
/platform-admin/login) — After sign-in, the server validates the Better Auth session and readsplatformAdmin/hipaaAdminfrom the Postgres user record; signs out and shows an error if neither is set. - Layout guard (
platform-admin/layout.tsx,hipaa-admin/layout.tsx) — Server-side session check on every navigation; redirects to login if the field is absent. - API middleware —
withPlatformAdmin()(Better Auth session +platformAdminfield) andwithHipaaAdminAuth()(httpOnly HIPAA session cookie validated via Firestore hash lookup) on every request.
Backend Enforcement
All tenant-scoped permissions are enforced server-side. The middleware chain is:
Request → withAuth HOF → permission / role check → handler
Key files:
| File | Purpose |
|---|---|
src/middleware/authMiddleware.ts |
withAuth HOF (tenant RBAC), withPlatformAdmin HOF, withHipaaAdminAuth HOF |
src/lib/rbac/role-permissions.ts |
Flat permission arrays per role |
src/lib/rbac/checkAccess.ts |
canPerformAction helper (ownership + role checks) |
src/lib/modules/default-permissions.ts |
Per-module, per-role view/create/edit defaults |
src/app/api/admin/modules/role-permissions/route.ts |
Owner-only module access overrides stored in Firestore |
src/app/api/admin/publish/assessment/route.ts |
Admin-only assessment publishing |
src/app/api/admin/publish/policy/route.ts |
Admin-only policy publishing |
src/lib/services/policy-service.ts |
Self-approval guard (submitter ≠ approver) |