fix(db): bootstrap migrations on local-tier gateway startup #510
Reference in New Issue
Block a user
Delete Branch "fix/db-bootstrap-migrations"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Fresh
mosaic gateway install(npm) leaves the gateway DB schema empty — sign-in 500s withrelation "users" does not exist. Five stacked bugs on the local (PGlite) tier:packages/db/package.jsonfiles: ["dist"]excluded thedrizzle/SQL migrations from the published tarball.runMigrations()only supports postgres-js — unusable for embedded PGlite.apps/gateway/src/database/database.module.tsnever invoked migrations at startup.createPgliteDbdidn't load pgvector, so migration 0001'sCREATE EXTENSION vectorfailed.check_safe_enum_useon migration 0009 (ALTER TYPE ADD VALUE 'pending'→SET DEFAULT 'pending'in the same tx).Changes
drizzle/in the published tarball.createPgliteDbloads@electric-sql/pglite/vector.runPgliteMigrations(handle)walks the Drizzle journal and runs each statement-breakpoint chunk through PGlite'sclient.exec()(autocommit per statement). Records intodrizzle.__drizzle_migrationsfor interop with the postgres-js path. Per-statement try/catch surfaces which statement of which migration failed.DatabaseModuleruns migrations inOnModuleInitbeforeapp.listen(). Local tier: explicitrunPgliteMigrationsthenstorageAdapter.migrate(). Postgres tier: juststorageAdapter.migrate(), which already callsrunMigrations(url)internally — no double-call.packages/storage/src/test-utils/pglite-with-vector.ts. The "intentionally not exported" rationale is moot now that migration 0001 forces pgvector load anyway. The integration test usescreatePgliteDb+runPgliteMigrationsfrom@mosaicstack/db.Tests
packages/db/src/migrate.test.ts— BetterAuth tables exist after migrate; idempotent (re-runs 0009); partial-failure surfaces statement-level context and leaves no ledger row.QA
End-to-end on a fresh PGlite install:
[DatabaseModule] Applying PGlite schema migrations...thenInitializing storage adapter (pglite)...in startup log.GET /api/bootstrap/status→{"needsSetup":true}HTTP 200 (was 500 withrelation "users" does not exist).POST /api/bootstrap/setupreaches Zod validator (was 500), confirming the request reached past the table-existence check.Scope
This PR fixes the local (PGlite) tier end-to-end. Postgres-tier first-install still has:
runMigrations().when(1745280000000) < 0008's (1776822435828), somigratePostgresskips bycreated_at < folderMillisand would skip 0009 forever after 0008 lands.Both flagged inline in
migrate.ts:31-35and in the design doc atscratchpads/fix-db-bootstrap-migrations.md. Needs a separate change with real-Postgres validation.Test plan
pnpm --filter @mosaicstack/gateway... build~/.config/mosaic/gateway/pglite/to simulate fresh installcurl http://localhost:14242/api/bootstrap/status→ expect{"needsSetup":true}HTTP 200mosaic auth users createinteractive flow and confirm user is created🤖 Generated with Claude Code
Fresh `mosaic gateway install` (npm) left the gateway DB schema empty — sign-in 500'd with `relation "users" does not exist`, and every entry point (auth, bootstrap setup) failed because they all query the users table first. Five stacked bugs on the local (PGlite) tier: 1. `packages/db/package.json` `files: ["dist"]` excluded the `drizzle/` SQL migrations from the published tarball. 2. `runMigrations()` only supports postgres-js — unusable for embedded PGlite. 3. `apps/gateway/src/database/database.module.ts` never invoked migrations at startup. 4. `createPgliteDb` didn't load pgvector, so migration 0001's `CREATE EXTENSION vector` failed. 5. Drizzle's PG migrator wraps every migration in one outer transaction, which trips Postgres' `check_safe_enum_use` on migration 0009 (`ALTER TYPE ADD VALUE 'pending'` → `SET DEFAULT 'pending'` in the same tx). Changes: - Ship `drizzle/` in the published tarball. - `createPgliteDb` loads `@electric-sql/pglite/vector`. - New `runPgliteMigrations(handle)` walks the Drizzle journal and runs each statement-breakpoint chunk through PGlite's `client.exec()` (autocommit per statement). Records into `drizzle.__drizzle_migrations` for interop with the postgres-js path. Per-statement try/catch surfaces which statement of which migration failed. - `DatabaseModule` runs migrations in `OnModuleInit` before `app.listen()`. Local tier: explicit `runPgliteMigrations` then `storageAdapter.migrate()`. Postgres tier: just `storageAdapter.migrate()`, which already calls `runMigrations(url)` internally — no double-call. - Removed `packages/storage/src/test-utils/pglite-with-vector.ts`. The "intentionally not exported" rationale is moot now that migration 0001 forces pgvector load anyway. The integration test uses `createPgliteDb` + `runPgliteMigrations` from `@mosaicstack/db`. Tests: BetterAuth tables exist after migrate; idempotent (re-runs 0009); partial-failure surfaces statement-level context and leaves no ledger row. QA on a fresh PGlite install: - `Applying PGlite schema migrations...` then `Initializing storage adapter (pglite)...` in startup log. - `GET /api/bootstrap/status` → `{"needsSetup":true}` HTTP 200 (was 500). - `POST /api/bootstrap/setup` reaches Zod validator (was 500). Scope: this PR fixes the local (PGlite) tier. Postgres-tier first install still has the outer-transaction problem and a journal ordering bug (0009's `when` < 0008's). Documented inline as TODO and in the scratchpad — needs a separate change with real-Postgres validation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>