You're reading docs for Nextly Alpha. APIs may change between releases.
Guides
Production migrations
Ship database schema changes to production safely with the Nextly migration CLI. Forward-only model, GitHub Actions, Vercel build step, and other-platform patterns.
Nextly's schema-evolution model splits responsibilities cleanly between development and production:
Local development:next dev + Nextly's HMR pipeline auto-applies schema changes as you edit nextly.config.ts.
Production: schema changes are committed as .sql files in your repo. A CI/CD step (or your laptop) runs nextly migrate against the production database before the new app code is deployed.
By default the deployed Next.js app does not touch the schema — you apply migrations in CI/build, before deploy (Patterns 1–3). Two production-grade options make this safe and flexible:
A migrate lock guards every nextly migrate, so two runs never apply schema at once. It's a pooler-safe lock row (works through Neon/Supabase PgBouncer) with a TTL, plus a --force-unlock escape hatch.
Optional run-on-boot (db.runMigrationsOnBoot, opt-in) applies pending migrations during app startup in production — safe across multiple instances thanks to the lock. See Pattern 4.
This page covers the common deploy patterns. Pick whichever fits your stack.
Wipes all tables and re-applies every migration. Local-dev only. Confirmation required.
nextly migrate:down
Rolls back the most-recently-applied migration(s) using their -- DOWN section. CLI-only; never runs on boot. See Rolling back a migration.
Global flags inherited from the root nextly command: --config <path> (custom nextly.config.ts path), --cwd <path>, --verbose, -q/--quiet.
nextly migrate:down reverts a single migration at a time (or the last N via --step). It is a manual, CLI-only tool — it never runs on boot. In production, prefer rolling forward with a new corrective migration, or restore from a backup; rollback restores schema shape, not row data. See Rolling back a migration.
nextly migrate -- --dry-run (preview without executing), --step <n> (apply only N pending migrations), --force-unlock (clear a stale migrate lock left by a crashed run, then migrate).
nextly migrate:create -- [name] positional or --name <name> flag, --blank (empty file for custom SQL), --non-interactive, --accept-renames.
nextly migrate:status -- --json (machine-readable output for CI scripting).
nextly migrate:resolve -- exactly one of --applied <file>, --rolled-back <file>, --failed-cleanup <file>; --skip-verify (with --applied, skip the live-vs-snapshot check).
nextly migrate:down -- --step <n> (roll back the last N migrations), --allow-data-loss (required when the DOWN drops a table or column), --yes (required in production), --dry-run (show targets + DOWN SQL without executing), --force-unlock.
Most teams should use this. The migration runs on a CI machine that explicitly has your prod DB credentials; if it fails, the Vercel deploy never happens. Old code keeps serving traffic on the old schema.
nextly migrate:check verifies file integrity + no uncommitted schema changes.
nextly migrate applies pending .sql files against the prod DB.
Vercel deploys the new app code.
If step 2 or 3 fails, step 4 doesn't run.
Setting NEXTLY_APPLIED_BY is optional but recommended. It populates the applied_by column in nextly_migrations so post-incident debugging can answer "which CI job applied this row?" When unset, the runtime falls back to GITHUB_ACTOR -> USER -> hostname.
The default scaffold ships this build script in package.json:
{ "scripts": { "build": "nextly migrate && next build" }}
Configure DATABASE_URL as a Vercel environment variable (available at build time). Every Vercel deploy applies pending migrations before compiling Next.js.
Trade-offs:
Zero CI setup. Push to GitHub, Vercel does everything.
Multiple Vercel deploys can race if you push twice in rapid succession (e.g. preview + production, or two PRs merging close together). Nextly's filename UNIQUE constraint catches double-insert; the second build will exit non-zero.
The Vercel build machine needs prod DB access from its IP range (check with your DB host).
For long-running servers/containers you can have the app apply pending migrations
during startup instead of (or in addition to) a CI step. Opt in via config:
Production only (NODE_ENV === "production") — a no-op in development.
Pending migrations apply during initialization, under the migrate lock, so
multiple instances starting together are safe: one applies while the others
wait, then all serve with the schema ready.
Failure-safe: a failed boot migration is logged and the app continues (it
does not crash the process); resolve with nextly migrate.
Tune lock takeover with db.migrateLockTtlSeconds (default 900s).
Serverless caveat. Running migrations on boot adds work to cold starts. On
platforms with many short-lived instances (e.g. Vercel), prefer the CI patterns
above. Boot-time migrations suit long-running servers/containers.
If your nextly.config.ts differs by environment (e.g. a plugin enabled only in
production), a migration generated in one environment can miss the other's
schema. Three ways to handle it:
Edit the generated migration after migrate:create to include the
environment-specific changes.
Temporarily enable the production env vars locally when generating, so the
migration captures the production shape.
Use separate migration files per environment, applied in the matching
environment.
Do NOT put nextly migrate in startCommand. startCommand runs on every container start and would race across replicas, which is precisely the failure mode this guide is designed to avoid.
Don't call nextly migrate from your deployed app's runtime. It's a CLI tool, not a runtime API. Nextly enforces this with an ESLint rule for code in init/, route-handler/, dispatcher/, api/, actions/, direct-api/, routeHandler.ts, and next.ts.
Don't edit applied migration files. The hash check catches this and aborts with MIGRATION_TAMPERED. To change something already applied, write a new corrective migration.
Concurrent nextly migrate runs are guarded by the migrate lock. A second concurrent run won't apply schema at the same time — it either errors with NEXTLY_MIGRATE_LOCK_BUSY (CLI) or waits (boot run). If a crashed run leaves a stale lock, clear it with nextly migrate --force-unlock. (The lock is a pooler-safe row with a TTL; the filename UNIQUE constraint remains a second line of defense.)
nextly migrate is forward-only. When a file partially applies or the live
schema drifts, use nextly migrate:resolve to fix the bookkeeping in
nextly_schema_events — it never runs migration SQL itself. All three modes
are idempotent (re-running a no-op exits 0) and take the same lock as migrate.
If nextly migrate fails partway through:
The failure is recorded as a file_apply event with status='failed' and structured error_json containing the SQL state and message.
Inspect with nextly migrate:status --verbose. The error JSON tells you what broke.
Pick the recovery that matches what actually happened:
The file actually applied but wasn't recorded (e.g. the process died after the DDL committed): nextly migrate:resolve --applied <file>. This verifies the live schema matches the file's target snapshot before recording it. Pass --skip-verify only if you have manually confirmed the state.
A failed attempt is blocking retries and the .sql needs editing first:nextly migrate:resolve --failed-cleanup <file> flips the stuck failed row to rolled_back. Edit the file, then run nextly migrate again.
You need to re-run a file that was recorded as applied: nextly migrate:resolve --rolled-back <file> — the next nextly migrate treats it as pending again.
Otherwise, fix the SQL (revert + regenerate via migrate:create) or the underlying database state, then re-run nextly migrate.
For MySQL specifically, partial state is possible because MySQL DDL auto-commits per statement. Manual cleanup may be needed; see MySQL caveats for the recovery playbook.
User migration files contain user-schema only — core system tables are owned by the Nextly package version and reconciled by nextly migrate Phase 1. If nextly migrate reports core schema drift after upgrading from a very early alpha (e.g. a hand-edited bundled file), run nextly upgrade --reconcile-core. It reconciles core in dev-loose mode and prompts for confirmation on each destructive operation. Use it only when migrate reports core drift.
nextly migrate:create writes a -- DOWN section into each generated migration
(the inverse of its -- UP). nextly migrate:down reverts the most-recently
applied migration using that section:
# Roll back the last migrationnextly migrate:down# Roll back the last 3nextly migrate:down --step 3# Preview without executingnextly migrate:down --dry-run
Rollback restores schema shape, not data. Reverting an added column drops
it (and its data); reverting a dropped column re-adds an empty column — the old
rows are gone. For this reason:
A migration whose DOWN drops a table or column requires --allow-data-loss.
In production (NODE_ENV=production), migrate:down requires --yes.
A migration with an empty -- DOWN (data-only or blank) is irreversible —
migrate:down refuses it. Roll forward with a corrective migration instead.
After a successful rollback, the migration is recorded as rolled_back in
nextly_schema_events and becomes pending again, so the next nextly migrate
re-applies it.
In production, prefer rolling forward (a new corrective migration) or
restoring from a backup. migrate:down is a manual, CLI-only break-glass tool;
it never runs on boot.
migrate:down handles single-step rollback, but for production incidents the
safer path is usually to roll forward -- write a new migration that fixes the
previous one. Examples:
Accidentally added a NOT NULL column without a default and the deploy failed -> write a new migration that backfills the column then re-applies the constraint.
Renamed a column too aggressively and broke a downstream service -> write a new migration that adds the old column name back as a copy until consumers are updated.
Need to drop a table you added in a recent migration -> write a new migration with DROP TABLE.
The audit trail in nextly_schema_events keeps both rows, so post-incident review can see exactly what was applied when, and by which CI job.