Skip to content

RBAC & super admin

Authorization is enforced in two layers: JWT role/permission at the HTTP edge, and RLS in the database. If one fails, the other holds.

Role model

Four ranked roles — 1 is the highest:

RoleSuperAdmin = 1  // manages admin users
RoleAdmin      = 2  // all permissions
RoleManager    = 3
RoleUser       = 4  // default for new eID users
  • IsAdmin() is true for both admin and super admin — a super admin inherits the admin JWT / RLS / permission paths.
  • IsSuperAdmin() is true only for super admins and gates the /superadmin surface.
  • Role ID 0 is the sentinel for legacy claim-less tokens; the RBAC middleware maps it down to RoleUser.

Dynamic permissions

Beyond the coarse ranking, the rbac usecase resolves a role's permission set from the database (roles · permissions · role_permissions).

Permission Category What it unlocks
dashboard.view general Dashboard
settings.manage general Settings, themes, AI prompts
users.manage administration User management
roles.manage administration Roles and permissions
gateway.manage administration API gateway, OAuth client registry
registry.view administration Viewing the service registry
registry.manage administration Editing and publishing the registry
relay.view administration SLA dashboard
relay.manage administration Relay routing
gov.catalog administration Managing the service catalogue
gov.review management Reviewing citizen applications (officer)
manager.view management Manager area
personal.view personal Personal area (/me/*)

Default grants for system roles:

Role Permissions
admin The entire catalogue — resolved automatically, no explicit rows
manager dashboard.view · manager.view · users.manage
user dashboard.view · personal.view

Using it on routes

r.With(mw.RequirePermission(resolver, "registry.manage")).
  Post("/services", h.CreateService)

r.With(mw.RequireAdmin()).Get("/audit", h.ListAudit)
r.With(mw.RequireSuperAdmin()).Route("/superadmin", )
  • RequirePermission lets admins through — they hold every permission.
  • If the resolver's database read fails it fails closed.
  • The frontend builds its menu from the same permission map in lib/navigation.ts, verified by navigation.test.ts.

Role management API

Endpoint Permission What
GET /v1/rbac/me authenticated My role and permissions
GET /v1/rbac/roles roles.manage List roles
GET /v1/rbac/permissions roles.manage Permission catalogue
POST /v1/rbac/roles roles.manage Create a role
PUT /v1/rbac/roles/{id} roles.manage Rename / describe
PUT /v1/rbac/roles/{id}/permissions roles.manage Set the permission set
DELETE /v1/rbac/roles/{id} roles.manage Delete a role

System roles (is_system = true) cannot be deleted. Every mutation is audited.

User management

Endpoint Permission
GET/POST /v1/admin/users users.manage
PUT /v1/admin/users/{id}/role users.manage
PUT /v1/admin/users/{id}/active users.manage
DELETE /v1/admin/users/{id} users.manage (soft delete)

Super admin

The super admin is the single role that manages admin users. Its account lives in the separate superadmin_accounts table — so the same person can be an eID admin and a super admin through a different method.

Endpoint What
GET/POST /v1/superadmin/admins List / create admin users
GET/POST /v1/superadmin/admins/by-register By national ID
PUT /v1/superadmin/admins/{id}/grant Grant
DELETE /v1/superadmin/admins/{id} Revoke
GET/POST /v1/superadmin/invites Invite allow-list
DELETE /v1/superadmin/invites/{email} Cancel an invite
GET/PUT /v1/superadmin/access-mode Platform access mode

Hardening:

  • Every mutation is written to the audit log.
  • MFA (TOTP) is mandatory at sign-in; recovery codes are stored hashed.
  • The first super admin is bootstrapped only from SUPERADMIN_EMAIL / the database — it cannot be created through the API.
  • See eID sign-in for the onboarding flow.

Adding a permission

Insert the row into permissions in a migration, gate the route with RequirePermission in the backend, and add the menu condition in lib/navigation.ts. Miss any of the three and the UI shows a page whose API returns 403.