Migrations
Voltro's planner-based migration system — diff your declared schema against the live DB, classify each change, refuse-to-apply anything risky without explicit intent. Dev auto-applies, prod refuses.
Voltro's migrator does not generate or apply migration files. Instead, on every voltro dev boot — and any time you run voltro db plan — it:
- Introspects the live database via
information_schema.*(or the dialect-specific equivalent), building aSchemaSnapshot. - Diffs that against your declared schema (every
*.entity.ts/*.schema.tsfile in the project plus the framework's bookkeeping tables). - Classifies each pending DDL op into one of seven
OperationClasses —safe,needs-default,needs-backfill,needs-rename-annotation,lossy,online-required,multi-step. - Refuses to apply anything that can't be made safe automatically. The diff output tells you exactly which DSL annotation to add (
.backfill(),.renamedFrom(),dropped(), …). - Applies the rest under an advisory lock + records the result in
_voltro_migration_planswith a fingerprint of the post-apply schema.
There are no generated SQL files to commit, no migrations/ directory to rebase, no checksum manifest to repair. The source of truth is your schema TypeScript; the DB is the slave.
The four-phase model
┌───────────────┐ ┌─────────────────┐ ┌──────────────┐ ┌──────────────┐
│ declare │ → │ plan (diff + │ → │ review │ → │ apply │
│ schema in TS │ │ classify) │ │ (CLI / UI) │ │ (dev: auto, │
│ │ │ │ │ │ │ prod: CLI) │
└───────────────┘ └─────────────────┘ └──────────────┘ └──────────────┘- dev: phases 2–4 are automatic on boot. A blocked plan refuses the boot with a structured error pointing at the fix. When the declared schema is unchanged since the last apply, dev short-circuits on a fingerprint check (one indexed query) and skips the full introspect entirely — so reboots against a large schema stay fast. Force a full re-introspect (drift recovery) with
VOLTRO_MIGRATE_FORCE=1. - prod: phase 2 (planning) is automatic, but phase 4 (apply) NEVER happens during a serving process. The fingerprint of the declared schema must already match
_voltro_migration_plans.fingerprintfrom a prior explicitvoltro db apply— mismatch → refuse to boot.
This is the constraint repeated across the docs: schema changes mid-rolling-deploy without review are not allowed.
A first session
# 1. Edit apps/api/database/users.entity.ts — add a required column.
export const users = table('users', {
id: id(),
email: text(), // NEW: required, no default
})
# 2. voltro dev. The planner classifies "ADD COLUMN email NOT NULL"
# on a populated table as `needs-backfill`. No backfill declared
# → boot refuses with:
#
# auto-migrate: REFUSED — 1 blocked operation(s):
# - add-column [users]: NOT NULL column on a table whose row count is unknown
# fix: declare `email: <type>().backfill(sql\`...\`)` OR `.default(value)`
# so existing rows survive the migration
voltro dev .
# 3. Add the backfill annotation in the schema:
export const users = table('users', {
id: id(),
email: text().backfill(sql`'unknown-' || id || '@local'`),
})
# 4. Boot again. The planner classifies the same op as `needs-backfill`
# with a declared backfill — applier runs ADD nullable → UPDATE via
# the SQL expression → SET NOT NULL inside one transaction.
voltro dev .Same column. Same migration. The first attempt refuses loudly; the second succeeds silently. The DSL annotation IS the migration plan.
The seven operation classes
Every pending op is stamped with one of seven OperationClass values —
safe, needs-default, needs-backfill, needs-rename-annotation,
lossy, online-required, multi-step — which drives whether it
auto-applies or refuses-to-plan pending a DSL annotation.
The full trigger + default-policy table, with a worked example and the exact fix for each blocked case, lives on the dedicated Operation classes page.
The CLI surface
# Planner-based (declarative diff)
voltro db plan # diff + color-coded classes + fix hints
voltro db plan --against <url> # diff vs a remote env's inspect endpoint (see cross-env-sync)
voltro db apply # execute (dev only — refuses on NODE_ENV=production)
voltro db apply --note '...' # apply with a freeform note recorded in history
voltro db plans [--limit 20] # history from _voltro_migration_plans, newest first
voltro db drift # live-vs-applied fingerprint check — exit 4 on drift
voltro db squash --before <date> # consolidate history into one snapshot
voltro db restore-snapshot <id> # restore VOLTRO_SOFT_DROP=1 columns from a plan
# File-based escape hatch — migration() up/down files under migrations/
voltro db files # apply pending migration() files
voltro db rollback-file <id> # run a migration() file's down body
# defineMigration step runner (separate system, _voltro_migrations table)
voltro db migrate # apply pending *.migration.ts steps
voltro db rollback [--to <id>] # undo applied *.migration.ts steps
voltro db status # list applied / pending *.migration.tsplan + apply are the planner-based commands. There are TWO distinct
file-based runners, intentionally not unified: the migration() runner
(files / rollback-file, records into _voltro_migration_plans with
source: 'file') is the escape hatch the planner points you at for
table-splits and cross-table data moves; the defineMigration runner
(migrate / rollback / status, its own _voltro_migrations table)
runs hand-authored *.migration.ts step files. See File-based
migrations for the migration() path — the one most
apps reach for.
Note: voltro db plan is flagless beyond --against / --token —
there is no --json or --sql, and voltro db apply takes only
--note. The prod flow is a plain voltro db apply run as an explicit
deploy step (prod pipeline), not a pre-serialised
plan file.
Where to go next
| Topic | Page |
|---|---|
| Every operation class with concrete examples + each fix | Operation classes |
| SQL vs JS backfill, performance, batch tuning | Backfill |
.renamedFrom() + dropped() lifecycle + when to remove the markers |
Rename and drop |
| MySQL implicit commit, SQLite table rewrite, per-dialect atomicity matrix | Multi-dialect strategy |
| CONCURRENTLY / batched backfill / shadow-column for large tables | Online migrations |
| File-based escape hatch for table-split / merge / data moves | File-based migrations |
| The plan-review-apply pipeline for production | Prod pipeline |
| Local devtools dashboard walkthrough | Devtools UI |
| Cloud dashboard walkthrough + multi-tenant boundaries | Cloud UI |
| What can be reversed (and why planner plans have no auto-rollback) | Rollback |
| Drift detection + recovery | Drift |
| Consolidating an aged history into one snapshot | Squashing |
VOLTRO_SOFT_DROP=1 + restore-snapshot — the only path that recovers dropped-column data |
Soft-drop recovery |
voltro db plan --against <env-url> for pre-deploy preview |
Cross-environment sync |
| The most common "why is my boot refusing?" cases | Troubleshooting |