Skip to main content
View as Markdown

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.

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

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.