Schema Migrations
SQL files in migrations/, applied by scripts/migrate.ts via bun run migrate.
Run
bun run migrateIdempotent — running twice in a row is a no-op the second time.
Tracking table
schema_migrations (version 000). Source: migrations/000_schema_migrations.sql.
CREATE TABLE IF NOT EXISTS schema_migrations (
version TEXT PRIMARY KEY,
name TEXT,
applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);name is nullable so legacy migrations that insert without supplying a name still work (e.g. 006_domain_registry.sql).
How the runner works
File: scripts/migrate.ts.
ensureTrackingTable()— bootstrapschema_migrationsif missing.- Read all
*.sqlfrommigrations/, sort by filename. - Compare against
SELECT version FROM schema_migrations. - For each unapplied file: stream into
psql -v ON_ERROR_STOP=1 -f <file>. - Insert row into
schema_migrationson success.
Why psql, not Bun.sql
// scripts/migrate.ts:114-117
// psql honours embedded BEGIN/COMMIT and CREATE INDEX CONCURRENTLY without
// wrapping in an implicit transaction (unlike Bun.sql.unsafe's extended-protocol path).Bun.sql is still used for the schema_migrations bookkeeping itself.
Recent fixes
- 007 collision resolved — duplicate version 007 was renamed to 009.
- CREATE INDEX CONCURRENTLY — was previously wrapped in
BEGIN/COMMIT, which Postgres rejects. Fixed by streaming bodies throughpsql.
Mark-only mode
bun run migrate --mark-applied <version>Records a row in schema_migrations without executing DDL. Used to onboard a database that already has the schema.