migrate
Migrates the databases of what is deployed from here.
pnpm dist migrate [--build] [name...]
pnpm dist migrate --status
pnpm dist migrate --rollback <name> [--steps N | --to TAG]
Run where an app is built, it migrates that app. Run where a monolith is built, it migrates every app the monolith depends on that declares migrations — and the monolith's own, if it has any — each into the database that app uses, which may be the same one.
Migrating: this is a Vercel production build.
@fairgarden/id applied 0003_passkey_names (DATABASE_URL_UNPOOLED)
@fairgarden/members up to date (DATABASE_URL_UNPOOLED)
In the build
fg-dist init gives the root's build and modular:build a step after
turbo's, which runs each app's (or the monolith's) migrate script — after the
environment check and the build itself:
"build": "pnpm --filter=./apps/monolith run --if-present check-env && turbo run build --filter=./apps/monolith && pnpm --filter=./apps/monolith run --if-present migrate"
After the build, so a build that fails to compile changes nothing. Outside
turbo, for two reasons. turbo keeps from a task every variable its turbo.json
does not list, and an app may name its database anything — kept from the
migrator, a variable that names one app's database could quietly leave it with
another's. And turbo can serve a task from its cache, which would skip a
migration for a database that has not seen it — a new preview's, say. A
migration failing fails the build, and the deployment with it.
With --build it only migrates where a build is meant to:
| A Vercel production build | migrates |
| A Vercel preview build | migrates with FG_MIGRATE=build |
| Anywhere else | migrates with FG_MIGRATE=build |
FG_MIGRATE=skip | never migrates |
A preview is opted in rather than out: set FG_MIGRATE=build for the Preview
environment once each preview has a database of its own, as Neon's branching
gives it. Until then, a preview reaching production's database would take a
branch's migrations there before the branch is merged.
Migrations run in the order drizzle-kit's journal lists them, and what has run has to be where that starts. A branch merged with a migration earlier in the journal than one already applied is refused rather than run after it, against a schema it was not written for: generate it again, after them. Applied ones moved about in the journal since they ran are refused too, by rollbacks as well, which would otherwise undo them in an order they did not run in.
On Vercel the Build Command has to run the root's build — pnpm build, the
default when the Root Directory is the repository — or migrating does not run
at all.
Rollbacks
A Vercel rollback promotes an older deployment without building anything, so it never undoes a migration. Every migration therefore has to work with the release before it as well as its own: add a column before the code that needs it, and drop one a release after the code stops using it.
To undo one by hand, roll back the app it belongs to:
pnpm dist migrate --rollback @fairgarden/id # its latest migration
pnpm dist migrate --rollback @fairgarden/id --steps 2
pnpm dist migrate --rollback @fairgarden/id --to 0002_policy_revisions
Each runs the migration's <tag>.down.sql, written by hand beside the one
drizzle-kit generated. A migration without one cannot be rolled back. Nor can
one the database has newer migrations on top of than this version knows, or one
edited since it ran — its down file undoes what the file says now, not what
ran. Roll back with the version that applied it.
--status changes nothing: a database that has never been migrated is left
without a journal, and every migration shows as pending.
Which database
An app's database is the first of its variables that is set — the same one the
app itself uses. Neon's pooled URL runs through PgBouncer, which does not keep
the session a migration's lock belongs to, so the variable's _UNPOOLED sibling
is used when there is one (DATABASE_URL_UNPOOLED, which Neon's integration
sets), or its _NON_POOLING one. Only that variable's own: another may name
another database.
With --build, it reads the environment files Next reads for a build —
.env.production.local, .env.local, .env.production, .env — beside where
it runs, after what is already in the environment, and with Next's own loader,
so $VARIABLE references expand as they do for the app. --status and
--rollback read the development files, as next dev does. On Vercel without a database
it fails the build; elsewhere an app without one starts its embedded database,
which migrates itself.
What an app declares
An app says where its migrations are in its package.json, so a monolith can
migrate it without loading it or its configuration:
"fairgarden": {
"migrations": {
"directory": "drizzle",
"table": "id_migrations",
"lock": 7031000001,
"database": ["FG_ID_DATABASE_URL", "DATABASE_URL", "POSTGRES_URL"]
}
}
directory is drizzle-kit's output. table is where the app records what has
run, with a hash of each migration: its own, so several apps' journals can share
a database. lock is the advisory lock it migrates under, so two builds take
turns; it has to differ from every other app's. database is the variables its
URL is read from, in the order the app itself reads them.
The migrator is @fairgarden/distribution/migrations, which the app also uses
for its embedded development database — the same code, so they agree on what
has run.
Exit codes
0 | migrated, up to date, or not a build that migrates |
1 | a migration failed, or an app has no database where it needs one |