# Commands (/docs/ship/migrations/commands)

---
title: Commands
description: 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.
kind: reference
package: questpie
---

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

```sql
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.
