auth/migrations.ts

Auth schema migrations.

Ordered list of {name, up} migrations for the fuz identity system tables. Consumed by run_migrations with namespace 'fuz_auth'.

The released chain is frozen — every schema change ships as an appended migration. Once a consumer holds a long-lived production database, an already-bootstrapped DB has recorded the existing migrations as applied, so editing a released migration's body in place is a silent no-op there: the CREATE TABLE IF NOT EXISTS doesn't re-run, the runner sees nothing new, and the new column never lands — a silent, total auth outage. (The deleted_at / deleted_by soft-delete columns were added to v0's base DDL this way; an older deployed DB never got them and every login broke.) So: never edit, rename, reorder, or re-purpose an entry in auth_migrations below. Add every additive change as a NEW appended entry using idempotent ALTER TABLE ... ADD COLUMN IF NOT EXISTS — a fresh bootstrap and an old deployed DB then converge on the same shape:

// v2: add display_name to account { name: 'account_display_name', up: async (db) => { await db.query('ALTER TABLE account ADD COLUMN IF NOT EXISTS display_name TEXT'); }, },

The /ready schema-drift probe (db/schema_ready.ts) is the runtime net: it fails the deploy loud when a live DB is missing a column the running code expects, rather than letting auth break silently. Discipline prevents the drift; the probe catches a lapse before cutover.

Migrations are forward-only (no down). Use IF NOT EXISTS / IF EXISTS for DDL safety. The name appears in error messages on failure. Dev/test DBs (no long-lived data) may still drop + re-bootstrap freely on a break — the freeze is the contract for the deployed chain, not local iteration.

Every entry is twinned on the Rust spine (fuz_auth/src/migrations.rs), and the two names must match byte for byte: the _testing_migration_tracker parity gate compares the (namespace, name, sequence) rows both spines record, and a divergence there breaks the swap-freely invariant (a consumer pointing either implementation at one database). Append on both sides in the same change, so no entry below repeats the rule.

view source

Declarations
#

4 declarations

AUTH_MIGRATION_NAMESPACE
#

auth/migrations.ts view source

"fuz_auth" import {AUTH_MIGRATION_NAMESPACE} from '@fuzdev/fuz_app/auth/migrations.js';

Namespace identifier for fuz_app auth migrations.

auth_migration_ns
#

auth/migrations.ts view source

MigrationNamespace import {auth_migration_ns} from '@fuzdev/fuz_app/auth/migrations.js';

Pre-composed migration namespace for auth tables.

auth_migrations
#

auth/migrations.ts view source

Migration[] import {auth_migrations} from '@fuzdev/fuz_app/auth/migrations.js';

Auth schema migrations in order.

  • v0: Full auth schema — account (with email_verified), actor, role_grant, auth_session, api_token, audit_log (with seq), bootstrap_lock, invite, app_settings, plus all indexes and seeds.
  • v1: role_grant_offer table for consentful grants; adds scope_id / scope_kind / source_offer_id / revoked_reason to role_grant and swaps the (actor_id, role) partial unique index for a scope-aware variant using the index-side 'GLOBAL' token + all-zeros sentinel UUID. The (scope_kind, scope_id) pair is enforced paired-null by role_grant_scope_kind_paired / role_grant_offer_scope_kind_paired CHECK constraints — both null for global, both non-null for scoped. The role_grant_offer table carries a superseded_at terminal state; its partial unique index is scoped by (to_account, role, scope_kind, scope, from_actor) so multiple grantors may coexist. scope_kind is informative-only in v1 (registry-membership validation against create_scope_kind_schema); v2 may add INSERT-time (role, scope_kind) enforcement.

reserved_migration_namespaces
#

auth/migrations.ts view source

readonly string[] import {reserved_migration_namespaces} from '@fuzdev/fuz_app/auth/migrations.js';

Migration namespaces reserved by fuz_app. Consumers passing migration_namespaces to create_app_backend must choose a name not in this list — the runtime check rejects matches with a thrown error. Typed as ReadonlyArray<string> (not a literal tuple) so .includes() accepts any consumer-supplied namespace string without a cast.

Depends on
#

Imported by
#