Docs

Migrations

The schema lives in apps/api/migrations/*.sql – plain SQL, applied in file-name order and recorded with a SHA-256 checksum in schema_migrations.

Applying

npm run db:migrate                         # = npm run migrate -w @3x/api (uses X3_DATABASE_URL_UNPOOLED)
npm run migrate -w @3x/api -- --status     # list applied / pending / changed
npm run migrate -w @3x/api -- --check      # exit 1 when something is pending or an applied file changed

A session advisory lock serializes concurrent runners. Alternatives: X3_AUTO_MIGRATE=1 (the first request migrates; other instances wait for the lock) or X3_MIGRATE_ON_BUILD=1 on Vercel.

Each file runs in a transaction unless its first line is -- x3:no-transaction (needed for CREATE INDEX CONCURRENTLY). An applied file must never change – --check reports a checksum mismatch, and the API refuses to auto-migrate over it.

Numbering

RangeOwner
0001initial schema (frozen)
01xxingest
02xxanalytics and conversions
03xxads
04xxMCP and OAuth
09xxreserved for partitioning

Your own deployment-specific migrations belong in a range you reserve (e.g. 08xx) so upstream updates never collide.

Roles

The first migration creates x3_sql_reader, a role without login that the MCP tool sql_query switches to (SET LOCAL ROLE). It can read analytics tables and views but no hashes, transient request context, secrets or credentials. The migrating role therefore needs CREATEROLE once (Neon's owner role has it).