--- type: database title: User Table description: The `user` table — accounts, roles, and statuses. tags: [database, user, auth] timestamp: 2026-07-21T14:43:00Z --- # Schema | Column | Type | Description | |----------------|----------|------------------------------------------------------------------| | `id` | TEXT PK | UUID v4 generated on creation. | | `username` | TEXT | Unique login name. | | `password_hash`| TEXT | Argon2id hash (memory/time/parallelism from env config). | | `role` | TEXT | `'admin'` or `'player'`. Default `'player'`. | | `status` | TEXT | `'enabled'` or `'disabled'`. Default `'enabled'`. | | `created_at` | TEXT | ISO 8601 timestamp (`strftime('%Y-%m-%dT%H:%M:%fZ','now')`). | # Constraints - Unique on `username`. - `role` is enforced application-side (see `AdminService.updateUserRole`). - `status` is checked by `AuthService.login` and `AuthService.refresh` — disabled users cannot log in or refresh. # Invariants - The application enforces the **last-admin invariant**: at least one enabled user with `role = 'admin'` must always exist. Attempts to demote or delete the last admin are rejected by `UsersService.enforceLastAdminInvariant` (used by `AdminService.updateUserRole` and `AdminService.deleteUser`). # First-admin bootstrap When the `user` table is empty of admins, the dedicated `POST /api/v1/setup/create-admin` endpoint (see [Setup Endpoint](/api/setup.md)) creates the very first admin and auto-issues a session. Once any admin row exists the endpoint becomes inert and returns `404 NOT_FOUND`, so the route is effectively hidden in production. The legacy `POST /api/v1/auth/register-first-admin` route is preserved for compatibility but the canonical first-admin flow now lives in the `SetupModule`. # See also - [Auth and Settings Tables](/database/auth-settings.md) - [REST API Overview](/api/rest-overview.md) - [Admin Endpoints](/api/admin.md)