Managing platform admins
Promote and demote super-admins from the console.
User.platformAdmin is a single boolean on every user. It's orthogonal to org-scoped roles — a platform admin is not automatically a member of any tenant org, and a tenant owner is not automatically a platform admin.
Promote a user
From /admin/users/[id], click Promote to admin. The mutation hits:
POST /api/v1/platform/admins/{userId}Audit: platform.admin.promoted with { targetUserId }.
Demote a user
From /admin/admins, click Demote on any row. The mutation hits:
DELETE /api/v1/platform/admins/{userId}Audit: platform.admin.demoted.
Invariants enforced server-side
These are guarded inside DemotePlatformAdminUseCase and the suspend / soft-delete paths in modules/iam/:
| Rule | Where enforced | Failure |
|---|---|---|
| Cannot demote yourself | use case + UI button disabled | SelfDemoteError (HTTP 409) |
| Cannot demote the last platform admin | atomic count under serializable txn | LastAdminError (HTTP 409) |
| Suspending the last admin is also blocked | SuspendUserUseCase | LastAdminError (HTTP 409) |
| Soft-deleting the last admin is also blocked | SoftDeleteUserUseCase | LastAdminError (HTTP 409) |
The "last admin" check is a single query — userRepository.countActivePlatformAdmins() — which counts only platformAdmin = true AND deletedAt IS NULL. Suspending an admin doesn't reduce this count (still platform admin, just inactive), but the gate is intentionally conservative: a platform admin who suspends themselves can't sign back in to undo it.