Skip to main content

Users and authentication tables

Six identity-domain tables (type notation defined in the overview conventions; ※ ALTER-added = exists only via store.Migrate's ALTER loop, not in schema.sql's CREATE TABLE).

users — master user table

Single source of truth for login, role, membership tier, credit balance, and 2FA — and the FK hub of the whole schema. 22 columns.

ColumnTypeNullableDefaultNotes
idTEXTno (PK)primary key, u_ + 12 hex chars
emailTEXTnologin email, unique database-wide
password_hashTEXTnobcrypt hash; OAuth-first-login accounts get a random password (paired with password_set=0 to force a new one)
nameTEXTno''display name
roleTEXTno'user'user / admin (validated at store/users.go:326)
statusTEXTno'active'active normal / pending email unverified (auth_handlers.go:364) / banned by an admin (admin_handlers.go:830) / deleting async purge in flight (users.go:1396)
token_verINTEGERno0token version: bumped on password change / ban / logout-everywhere; middleware compares it to the tv claim of every access token → instant cross-replica revocation
settingsTEXT (JSON)no'{}'per-user preferences JSON object (default model, tool mode, welcome-wizard flag onboarded and other frontend-defined keys; one-time backfill at store.go:796-805)
group_idTEXTno'ug_free'membership tier; soft reference to user_groups.id (no FK constraint)
group_expires_atINTEGER → BIGINTno0※ ALTER-added: end of a time-boxed membership granted by a redeem code / purchase (0 = never expires)
previous_group_idTEXTno''※ ALTER-added: tier to restore when the window expires
totp_secretTEXTno''base32 TOTP secret; empty = 2FA not configured
totp_enabledINTEGERno01 = login requires a TOTP code
password_setINTEGERno10 = OAuth-only account that never chose its own password; login forces the set-password flow
password_changed_atINTEGER → BIGINTno0unix seconds of the last password change (0 = never since signup)
last_seen_atINTEGER → BIGINTno0last authenticated activity (admin online status)
credits_permanentREALno0display mirror of the permanent balance (not authoritative)
credits_permanent_microsINTEGER → BIGINTno0authoritative permanent credit balance, fixed-point 1e-6
credit_cycle_anchorINTEGER → BIGINTnonow()current group's timed-credit cycle origin
quota_cycle_anchorINTEGER → BIGINTnonow()current group's model-quota cycle origin
sort_orderINTEGERno0admin drag-and-drop ordering (legacy rows flattened by a one-time backfill, store.go:788-795)
created_atINTEGER → BIGINTnonow()creation time
  • PK: id. FKs: none outgoing. Nearly every user-domain table references this one via user_id — usually ON DELETE CASCADE; billing_usage / usage_stats / payment_orders use SET NULL (account deletion anonymizes attribution while preserving records).
  • Unique/indexes: email UNIQUE (inline); idx_users_sort_order(sort_order, created_at DESC) created during migration (store.go:573).
  • Notes: deletion is an async multi-step flow — physical paths are first registered in pending_storage_cleanup, rows are then deleted, and CASCADE clears most children.

refresh_tokens — sessions and devices

Rotating refresh tokens and the "active sessions" view. 10 columns.

ColumnTypeNullableDefaultNotes
jtiTEXTno (PK)refresh-token id; rotating: refreshing invalidates the old jti
session_idTEXTno''session-family id; migration backfills legacy empties with the original jti (store.go:538-543)
user_idTEXTnoFK→users(id), ON DELETE CASCADE
expires_atINTEGER → BIGINTnoexpiry in unix seconds; issued with REFRESH_TTL = 720h (30 days)
revokedINTEGERno01 = revoked (single-device logout / admin kick)
created_atINTEGER → BIGINTnonow()issue time
user_agentTEXTno''device context ("active sessions" view)
ipTEXTno''login IP (XFF honored only when the direct peer is private; see the Cloudflare page)
locationTEXTno''best-effort geo (derived from reverse-proxy geo headers, if any)
last_seenINTEGER → BIGINTno0last refresh of this session
  • Indexes: idx_refresh_tokens_user_id(user_id), idx_refresh_tokens_user_session(user_id, session_id) (both created during migration, store.go:564-565).
  • Notes: unlike login_histories, rows are removed on logout/rotation — this is not the audit source of truth. Each access token also carries sid (session family); middleware checks it against the DB so per-device revocation works across replicas.

login_histories — successful-login audit

Immutable successful-login trail. 7 columns.

ColumnTypeNullableDefaultNotes
idTEXTno (PK)lh_ + 12 hex chars
user_idTEXTnoFK→users(id), CASCADE
login_atINTEGER → BIGINTnonow()login time
ipTEXTno''source IP
locationTEXTno''best-effort geo
user_agentTEXTno''client UA
methodTEXTno'password'login method: password / password_2fa / oauth / oauth_2fa (constants in store/login_histories.go:11-16)
  • Index: idx_login_histories_user_time(user_id, login_at DESC, id DESC).
  • Notes: logout and session rotation never delete these rows; administrators drill in from the user detail page.

oauth_providers — social login configuration

OAuth / social login providers configured by the admin. 18 columns.

ColumnTypeNullableDefaultNotes
idTEXTno (PK)oa_ + 12 hex chars
kindTEXTnogoogle / github / apple / oidc / oauth2 (internal/oauth/oauth.go)
nameTEXTnolabel shown on the login button; lower(trim(name)) unique
iconTEXTno''emoji or uploaded icon URL (custom providers)
client_idTEXTno''OAuth client id
client_secretTEXTno''plaintext; for Apple it holds the AuthKey .p8 private key (a client-secret JWT is minted at request time, TTL 30 min)
issuer_urlTEXTno''expected OIDC iss (generic kinds only)
jwks_urlTEXTno''trusted signing-key set URL (generic kinds only)
auth_urlTEXTno''authorization endpoint (oidc/oauth2 only; built-in kinds ignore this and use code defaults)
token_urlTEXTno''token endpoint (same rule)
userinfo_urlTEXTno''UserInfo endpoint (used by oauth2 kind)
scopesTEXTno''space-separated scope override
team_idTEXTno''Apple developer team id
key_idTEXTno''Apple AuthKey id
subject_namespaceTEXTno''internal trust-domain marker (distinguishes new vs legacy oidc rows; backfill MigrateLegacyOAuthProviderKinds)
enabledINTEGERno1show on the login page
sort_orderINTEGERno0button order
updated_atINTEGER → BIGINTnonow()update time
  • Index: idx_oauth_providers_name_unique (UNIQUE ON lower(trim(name))).
  • Notes: custom issuer/JWKS hosts are trusted only for oidc/oauth2 — a stale row-level override can never move a built-in endpoint (oauth.go:121-125). PKCE applies to google/oidc/oauth2; the ID-token verification path to google/apple/oidc.

Maps "provider row + immutable subject" to a local account. 5 columns.

ColumnTypeNullableDefaultNotes
provider_idTEXTno (PK part)soft reference to oauth_providers.id (no FK)
subjectTEXTno (PK part)provider-side stable user identifier
user_idTEXTnoFK→users(id), CASCADE
emailTEXTno''email snapshot at link time (not the match key)
created_atINTEGER → BIGINTnonow()first link time
  • PK: composite (provider_id, subject). Index: idx_oauth_identities_user(user_id).
  • Notes: keyed on the provider's immutable subject, so links survive email changes; unlinking goes through /api/me/identities.

passkeys — WebAuthn credentials

One row per user-registered passkey device. The table stores the credential public key and authenticator state; it never stores a biometric or device PIN. 9 columns.

ColumnTypeNullableDefaultNotes
idTEXTno (PK)credential row id
user_idTEXTnoFK→users(id), ON DELETE CASCADE
credential_idBLOBno (UNIQUE)raw WebAuthn credential identifier
public_keyBLOBnoCBOR-encoded credential public key
sign_countINTEGERno0authenticator signature counter; regressions help detect cloned credentials
authenticator_flagsINTEGERyesNULLsigned authenticator flags; NULL means a legacy row until its first verified assertion backfills the value
nameTEXTno''user-visible device label
created_atINTEGER → BIGINTnonow()registration time
last_used_atINTEGER → BIGINTno0last successful assertion (0 = never)
  • Indexes: idx_passkeys_user(user_id) and the unique constraint on credential_id.
  • Security behavior: passkey login requires a secure context (HTTPS or localhost) and user verification. A passkey is a complete authentication factor, so a successful passkey assertion does not additionally require TOTP; password + TOTP remains unchanged. The platform-wide passkey switch in the registration policy gates registration, listing, and login.
  • Deletion: deleting a user cascades to all of their passkeys. Removing one device from Account settings deletes only that credential and leaves other sessions intact.