Skip to content

Admin, RBAC, audit

The management surface rests on four roles, a dynamic permission catalogue, and a hash-chained audit log.

Four roles

superadmin  →  admin  →  manager  →  user
Role Responsible for
superadmin Appointing/removing admins, invites, platform access mode. The only role that does this.
admin Running the system — auto-resolves to every permission in the catalogue
manager Reviewing and deciding citizen applications (gov.review)
user Their own area (personal.view)

RBAC — dynamic permissions

Roles and permissions live in the database and change at runtime. The full catalogue is in the Capability map.

Method Path Permission
GET /api/v1/rbac/me signed in — own permissions
GET /api/v1/rbac/roles · /permissions roles.manage
POST PUT DELETE /api/v1/rbac/roles… roles.manage
PUT /api/v1/rbac/roles/{id}/permissions roles.manage

Permission checks are applied as route-level middleware (RequirePermission) rather than repeated in every handler, so a new endpoint is much less likely to ship unguarded. routes_authz_matrix_test.go pins that matrix down in tests.

UI: /admin/roles.

Admin surface

Method Path Permission
GET POST /api/v1/admin/users users.manage
PUT /api/v1/admin/users/{id}/role · /active users.manage
DELETE /api/v1/admin/users/{id} users.manage
GET PUT /api/v1/admin/ai/prompts · /{key} settings.manage
POST /api/v1/admin/ai/knowledge/reindex settings.manage

The AI prompt layers and the knowledge base are configured from the database — see AI pipeline. The guardrail layer is hardcoded and is never configurable.

UI: /admin/dashboard, /admin/users, /admin/settings, /admin/core.

Super admin

Method Path What
GET POST /api/v1/superadmin/admins List / add admins
GET POST /api/v1/superadmin/admins/by-register Look up / add by national register number
PUT /api/v1/superadmin/admins/{id}/grant Grant
DELETE /api/v1/superadmin/admins/{id} Revoke
GET POST DELETE /api/v1/superadmin/invites… Manage invites
GET PUT /api/v1/superadmin/access-mode Platform access mode (public / private)

Onboarding and MFA

The path to becoming a super admin is guarded in several layers:

  • Invite allow-list — onboarding cannot start from an uninvited email address, and a used invite cannot be reused.
  • eID proofPOST /api/v1/auth/superadmin/onboard/eid/start · /start-id · /poll (national ID or QR).
  • Google or email verification/onboard/google, /onboard/email/send · /verify.
  • TOTP MFA/onboard/totp/init · /verify, then POST /api/v1/auth/superadmin/mfa on every sign-in.

The TOTP secret is stored as AES-GCM ciphertext — plaintext never reaches the database. Recovery codes are shown once at creation; only their SHA-256 hash is stored, and a used code never works again.

Every onboarding endpoint carries the auth limiter (5/min).

UI: /superadmin/login, /superadmin/onboard, /admin/superadmin.

Audit

The audit log is hash-chained and append-only. Each entry carries the hash of the previous one, so editing or deleting a row in the middle breaks the chain.

Method Path What
GET /api/v1/audit Paginated listing (admin)
GET /api/v1/audit/verify Chain integrity check

verify returns { ok, broken_id } — on ok=false it points at the id of the first broken row.

The acting user is read from the request's RLS identity and request_id from the context automatically — callers do not pass them, which leaves little room for forged attribution.

Microsecond precision

Postgres timestamptz has microsecond precision. If the timestamp that goes into the hash is not truncated to match, VerifyChain reports false negatives from nanosecond drift — the code handles this deliberately.

UI: /admin/audit.

Security events

Client-side security signals (CSP violations and the like) are collected:

  • POST /api/v1/security/events — open to any signed-in user (RLS pins the user_id)
  • GET /api/v1/security/eventsadmin only

UI: /admin/security.

Appearance and branding

Colours, fonts and density are never hardcoded — they are set from the admin UI.

Method Path Permission
GET /api/v1/site/appearance public (the landing page needs it)
PUT /api/v1/site/appearance settings.manage
GET /api/v1/themes/active public
GET POST PUT DELETE /api/v1/themes… admin
PUT /api/v1/themes/{id}/active admin

Each user may keep their own override. UI: /admin/themes, /admin/settings.