Admin, RBAC, audit¶
The management surface rests on four roles, a dynamic permission catalogue, and a hash-chained audit log.
Four roles¶
| 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 proof —
POST /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, thenPOST /api/v1/auth/superadmin/mfaon 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 theuser_id)GET /api/v1/security/events— admin 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.