Commands
Every migrate command the CLI registers, the flags it takes, the ledger table it writes to, and what each one does when there is nothing to do.
Seven commands deal with the database schema. Each one takes
-c, --config <path>, which defaults to questpie.config.ts.
| Command | Also accepts | What it does |
|---|---|---|
questpie migrate:create | migrate:generate | Diffs the schema against the snapshot chain, writes a file |
questpie migrate | migrate:up | Runs pending migrations, then the search index migrations |
questpie migrate:down | Rolls back the last batch | |
questpie migrate:status | Prints executed and pending migrations | |
questpie migrate:reset | Rolls back every executed migration | |
questpie migrate:fresh | Reset, then up, then reset seed tracking | |
questpie push | Applies the schema straight to a development database |
The starters wrap all seven as package scripts with the config path filled in.
So bun run migrate and bun run migrate:create work from the project root.
Push is scripted as db:push.
migrate:create
| Flag | What it does |
|---|---|
-n, --name <name> | Names the file. Random name if you omit it |
--dry-run | Prints the name and directory, writes no file |
--verbose | Accepted and currently unused |
--non-interactive | Answers any prompt with its default |
It writes two files. <timestamp>_<name>.ts in your migrations folder, and a
matching .json under migrations/snapshots/. The timestamp is
YYYYMMDDTHHmmss. The migration id is the name in camelCase with that
timestamp appended, so --name add_slug gives addSlug20260803T120000.
No name produces something like bright_azure_falcon. Readable, but say what
the migration does instead.
migrate:create never opens a database connection. It compares your schema
against the union of the on-disk snapshots and the snapshots embedded in the
migrations the app already imports. No difference means no file. It prints
No schema changes detected and stops.
migrate
| Flag | What it does |
|---|---|
-t, --target <migration> | Stops after this migration id, inclusive |
--dry-run | Prints the action and exits |
It checks the PostgreSQL version first and requires 15 or newer. Then it creates
questpie_migrations if it is missing and reads which ids are already there.
Each pending migration runs in one transaction. The transaction takes
pg_advisory_xact_lock, re-checks the ledger, runs up(), then inserts the
ledger row. Every migration in one run shares a batch number.
An unknown --target throws before anything runs. --dry-run prints the action
and the target only. It does not list the pending migrations.
After the migrations finish, migrate runs the search adapter's index
migrations. Those use IF NOT EXISTS, so they re-run on every migrate. The
CLI prints nothing about them. Only app.migrations.search() hands back the
applied and skipped lists. A failure that mentions an extension or a permission
is re-thrown with the statement attached.
migrate:down
| Flag | What it does |
|---|---|
-b, --batch <number> | Rolls back the batch you name. Must be a positive integer |
-t, --target <migration> | Rolls back to this migration id, inclusive |
--dry-run | Prints the action and exits |
With no flag it rolls back the last batch. That is every migration applied by
the last migrate run, not the last migration.
Each down() runs in its own transaction and deletes its ledger row. A ledger
row whose migration is no longer in the app gets a warning and a skip. Its row
stays behind, so the id still counts as executed.
down() reverses DDL. It cannot bring data back. A migration that dropped a
column rolls back to that column, empty.
migrate:status
Creates the ledger table if it is missing. Then prints the current batch, the executed migrations with their batch and time, and the pending ones.
migrate:reset and migrate:fresh
Both take --dry-run. reset rolls back every executed migration in reverse
order. fresh resets and then runs everything again. Through the CLI, fresh
also clears seed tracking, so your seeds can run again on the rebuilt tables.
push
| Flag | What it does |
|---|---|
-f, --force | Acknowledges the development warning |
-v, --verbose | Prints the SQL it is about to run |
Push loads your app, asks drizzle-kit for the statements that would bring the
live database in line with app.getSchema(), then applies the complete diff in
one PostgreSQL transaction. If any statement fails, every earlier statement in
that push is rolled back. Push records no migration history.
--force changes the output and nothing else. Push applies either way.
The diff is scoped to the schemas your own tables use, and the ledger table is
excluded by name. The pgboss schema is excluded too. Before applying, a second
check scans the planned statements. Anything that would drop, truncate or alter
framework or adapter state aborts the command with nothing executed.
The ledger table
CREATE TABLE questpie_migrations (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
batch INTEGER NOT NULL,
executed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);id is the primary key, so migration ids must be unique across your whole app.
name currently holds the same string as id. Every command that reaches the
database creates this table on demand, so there is nothing to set up.
When no migrations are registered
A project with no migration files has an empty migrations array. Every migrate
command, status included, prints No migrations found and exits without
touching the database.
Hand-written migrations
questpie add migration <name> writes an empty migration with up and down
stubs next to your other ones. Use it for DDL the generator cannot express, like
a data fix or an extension you install yourself.
A hand-written migration has no snapshot. Snapshots are what migrate:create
diffs against. So a change made only in hand-written SQL stays invisible to the
generator, and the generator may plan it again later.
Quieter output
Set QUESTPIE_MIGRATIONS_SILENT to 1, true or yes to drop the info and
warning lines. Errors still print. Under NODE_ENV=test the runner is silent by
default.
Client variables
One clientEnv call declares what the browser may see. Codegen writes one module per bundler, with that bundler's prefix already spelled out.
Zero downtime
A rolling deploy runs old and new code against one database at the same time. Sequencing a required column, a drop, a rename and a backfill so no request fails while that is true.