Skip to content

SCRUM-198 — admin-auth database schema

Plan ref: AA-2 (docs/11-admin-plane-plan.md). Stacked on SCRUM-197.

What changed

New migration services/admin_auth/migrations/00002_staff_schema.sql. (00001 stays the AA-1 no-op so any database that already ran SCRUM-197's migrate upgrades cleanly — verified below.)

Table Purpose Key constraints
staff_user one row per staff account lower(email) unique; roles ⊆ {viewer, live_ops, admin} and non-empty; root must hold admin; at most one root (unique index over a constant WHERE is_root); status ∈ {active, disabled}; lockout counters; TOTP columns
staff_invite one-time invite / password-reset link (only sha256(token) stored) invite ⇒ role set & no user_id; reset ⇒ user_id set; expires_at > created_at; pending-by-email index
refresh_session one row per refresh token; family_id for reuse detection token_hash unique; cascade on user delete; family + active-by-user indexes
mfa_recovery_code hashed single-use recovery codes PK (user_id, code_hash); cascade
signing_key Ed25519 public keys for JWKS same shape as services/auth (code reuse; keys are never shared between domains)
audit_log every mutating action same shape as Config's (Dashboard merges them)

No extensions (no citext): case-insensitive email uniqueness is a functional index.

How to verify

cd services/admin_auth
export ADMIN_AUTH_TEST_DATABASE_URL='postgres://USER:PASS@127.0.0.1:5433/admin_auth_test?sslmode=disable'
go vet ./... && go test -race -count=2 ./...     # -count=2: tests are re-runnable on a shared DB
Test Proves
TestMigrateIsIdempotent Up twice records each embedded migration once
TestMigrateUpDownUp full Down to 0 and back Up; schema usable after
TestEmailUniquenessIsCaseInsensitive A@x then a@x → 23505
TestRolesCheck {} and {superuser} → 23514; {viewer,admin} ok
TestSingleRoot second root → 23505 (skips itself if a root already exists; cleans up)
TestInviteChecks invite w/o role, reset w/o user_id, expires_at <= created_at → 23514
TestStaffDeleteCascades deleting a user removes its sessions and recovery codes

Upgrade path (done by hand during review): fresh DB → SCRUM-197 binary migrate (records 00001) → this branch's binary migrate → OK 00002_staff_schema.sql, version 2, all six tables present.

Results at time of writing

  • go vet, go test -race -count=2: pass (4 packages) against Postgres 16.

How it was built

DeepSeek run scoped (Landlock) to services/admin_auth (111 s, ~20k output tokens). Claude review changed three things: moved the schema from an in-place edit of 00001 into 00002 and removed a DROP SCHEMA public CASCADE the test had added to paper over that edit; corrected two comments (a wrong claim that signing keys are movable between services; "token only in the email" — there is no email); made the idempotency test count embedded files instead of hardcoding 1.