User Management
Sovrium provisions and manages users without ever requiring you to touch the database directly. The first administrator is created at boot (from environment variables, a one-time token, or the CLI); subsequent users are created, listed, role-assigned, and invited through the authenticated admin API. Every operation is RBAC-gated and audit-logged.
User management is only available when authentication is configured. The admin plugin is enabled automatically the moment an auth block exists — there is no separate flag to set.
Admin Bootstrap
The very first admin cannot be created by another admin (there is none yet) or by self-registration (you do not want anyone claiming the admin seat). Sovrium offers three complementary bootstrap paths for provisioning that first account on a fresh database.
| Path | When to use | Mechanism |
|---|---|---|
| Env-var bootstrap | Automated / IaC deploys | Set AUTH_ADMIN_EMAIL + AUTH_ADMIN_PASSWORD (+ optional AUTH_ADMIN_NAME); the admin is created on first boot. |
| One-time token | You do not want credentials in env vars | Boot with no AUTH_ADMIN_EMAIL and no users; a 64-hex token is printed in the startup banner and claimed once via POST /api/admin/bootstrap/claim. |
| CLI | Interactive provisioning | sovrium admin create <email> works against the configured database (or the default SQLite file) with no app.yaml required. |
Env-var bootstrap (no-config)
AUTH_ADMIN_EMAIL=admin@example.com
AUTH_ADMIN_PASSWORD=SecureP@ssw0rd!
AUTH_ADMIN_NAME=System Administrator # optional, defaults to "Administrator"
On first boot against a fresh database the server provisions the admin with a verified email and grants it access to every admin-only endpoint. On subsequent startups the path no-ops — it never creates a duplicate and never modifies an existing user, even if the email already maps to a different role. The success is logged without exposing the password.
| Env var | Description |
|---|---|
AUTH_ADMIN_EMAIL |
Email for the bootstrap admin. Required for the env-var path. Must be a valid email format. |
AUTH_ADMIN_PASSWORD |
Initial password. Required for the env-var path. Must meet the minimum length (8 characters). |
AUTH_ADMIN_NAME |
Display name. Optional — defaults to Administrator. |
Bootstrap is gated on a configured auth block. If auth is absent, or AUTH_ADMIN_EMAIL/AUTH_ADMIN_PASSWORD is missing, no admin is created — the server boots without one. The env-var path is a no-op when any user already exists.
One-time token bootstrap
When the server boots with no AUTH_ADMIN_EMAIL set and no users in the database, it generates a 256-bit cryptographically random token, prints it once in the startup banner, and accepts a single claim:
# Token appears in the banner as:
# → First-admin token (POST /api/admin/bootstrap/claim): <64-hex-token>
curl -X POST http://localhost:3000/api/admin/bootstrap/claim \
-H 'Content-Type: application/json' \
-d '{ "token": "<64-hex-token>", "email": "admin@example.com", "password": "SecureP@ssw0rd!", "name": "Admin" }'
Security properties:
- Only the SHA-256 hash of the token is persisted; the plaintext is printed to stdout exactly once and never logged.
- The token expires after 1 hour and can be claimed once — replays return
401. - Once any admin exists the route returns 404: the bootstrap window has closed, and even a leaked valid token cannot reopen it.
This is the path that makes "run the binary on a fresh server with only DATABASE_URL set, open the URL, build the app live" possible. See Database Infrastructure for the zero-config database that pairs with it.
Creating & Managing Users
Once an admin exists, all subsequent user lifecycle operations run through the admin API. Each endpoint requires an authenticated admin session — unauthenticated requests return 401, non-admin sessions return 403.
| Operation | Endpoint | Notes |
|---|---|---|
| Create user | POST /api/auth/admin/create-user |
Engineer chooses the password; no email is sent. Returns 201. |
| List users | GET /api/auth/admin/list-users |
Paginated (limit/offset), returns count metadata, supports search by email or name. |
| Get user | GET /api/auth/admin/get-user/:id |
Full detail incl. role, ban status, email-verified flag. 404 for unknown ids. |
| Set role | POST /api/auth/admin/set-role |
Assign admin / member / viewer (or any custom role). |
| Set password | POST /api/auth/admin/set-user-password |
Reset a user's password administratively. |
| List sessions | GET /api/auth/admin/list-user-sessions |
Active sessions for a user. |
| Revoke session | POST /api/auth/admin/revoke-user-session |
Force-logout a specific session. |
| Impersonate | POST /api/auth/admin/impersonate-user |
Start/stop impersonation for support workflows. |
auth:
strategies:
- type: emailAndPassword
defaultRole: member
Validation is enforced server-side. Create-user returns 400 for a missing/malformed email or missing password, and 422 when the email already exists. Roles assigned must be one of the app's declared roles — see Roles & RBAC.
Role Assignment
Sovrium ships three default roles — admin, member, viewer — and supports custom roles declared in the auth block. New users receive auth.defaultRole (falls back to member) unless an explicit role is supplied at creation. The first bootstrap admin is always created with the admin role and a verified email.
Roles drive every authorization decision in the platform: table permissions, per-field access, page access, and the admin API itself. See Roles & RBAC for the full role model and field-level permission matrix.
Invitation Flow
The create-user endpoint requires the admin to choose the customer's password and sends no email — unsuitable for real customer onboarding. The parallel passwordless invitation flow closes that gap: the admin issues an invitation with { email, name, role } (no password), Sovrium emails a single-use link, and the customer sets their own password and lands authenticated.
auth:
strategies:
- type: emailAndPassword
invitationTokenExpiry: '72h' # default lifetime; accepts '30s' / '15m' / '72h' / '7d' / ms
emailTemplates:
invitation:
subject: 'You are invited to join, $name'
text: |
Hi $name,
$inviterName invited you to join.
Set your password: $url
This invitation expires in 72 hours.
| Endpoint | Behavior |
|---|---|
POST /api/auth/admin/invite-user |
Accepts { email, name, role } (no password). Returns 200 with { user, invitationSent: true }. 401/403 for unauth/non-admin; 422 when the email already maps to a fully-onboarded user. |
POST /api/auth/admin/accept-invitation |
Backs the public /accept-invitation?token=... page. The customer sets a password and lands in an authenticated session. |
Invitation tokens reuse the auth.verification table (the same shape Better Auth uses for password reset), expire after invitationTokenExpiry (default 72h), and are single-use — replays return 400/410. Email substitution variables: $name, $url, $email, $inviterName. auth.allowSignUp: false does not block invitations — admin-driven user creation is always available when auth is configured.
Onboarding is decoupled from access assignment. Inviting a user creates the account but does not grant them any tenant scope. Wiring a user to data access is a separate step via the standard records API — an admin can invite first and assign later, or vice versa.
Related Pages
- Authentication Overview — strategies, configuration, the
authblock. - Roles & RBAC — role model and field-level permissions.
- Sessions — session lifetime, revocation, multi-device.
- Admin Dashboard — operator-grade read API over users, tables, automations.
- Activity Monitoring — audit trail of user and admin actions.
- Database Infrastructure — the zero-config database the no-config bootstrap pairs with.
- Environment Variables — full env-var reference incl.
AUTH_ADMIN_*.