Migrate

voltro migrate — apply the declared schema through the declarative differ (an alias of voltro db apply).

voltro migrate applies your declared schema (*.entity.ts / *.schema.ts / schema.ts) to the configured database. It is an alias of voltro db apply: it diffs the declared schema against the live database and emits the ALTERs, so a changed column or a new index actually lands.

Before 0.11.4 this command was a create-only apply (CREATE TABLE IF NOT EXISTS, no diffing), which meant a column or type change reported success having applied nothing. If you need that bootstrap-only behaviour for a brand-new database, it is now voltro migrate --create-only.

voltro migrate            # apply the discovered schema to the configured store
voltro migrate apps/api   # explicit app directory (defaults to cwd)

voltro migrate forwards its arguments to voltro db apply, so the same flags apply; --create-only selects the bootstrap-only emitter instead. The diff / plan / apply / drift / squash workflow lives under voltro db (see below).

For the deep dive on the schema DSL + day-to-day patterns, see Database / Migrations.

How discovery works

The migrator walks the project root (skipping node_modules, dist, .framework, etc.) for:

  • *.entity.ts / *.schema.ts / schema.ts — your table descriptors.
  • Feature files (*.workflow.tsx, *.cron.tsx, *.webhook.tsx) — their presence decides which framework-internal _voltro_* tables get created (workflow-run tables, schedule ledgers, etc.). _voltro_traces is the exception — env-gated, not feature-gated: it's created under voltro dev (durable trace history on by default) but NOT under voltro serve / voltro start (prod default off → set VOLTRO_TRACING_PERSIST=interesting to opt in). See Distributed tracing.

The core actors table is injected automatically when you didn't declare your own, and tenants is registered when present, so the audit / soft-delete / tenant mixins resolve their FK targets.

voltro dev already auto-migrates

On a SQL-backed store, voltro dev AUTO-APPLIES the discovered schema before any handler boots — so a standalone voltro migrate is rarely needed in the inner loop. It's the CI / ops-pipeline entry point.

Opt out of the dev auto-migrate when you ship schema through a separate reviewed pipeline:

VOLTRO_AUTO_MIGRATE=0 voltro dev .

The boot log then says auto-migrate: skipped (VOLTRO_AUTO_MIGRATE=0), and you run voltro migrate (or voltro db apply) explicitly.

Store + dialect selection

The dialect is resolved from DB_DIALECT (default postgres); the connection comes from DB_URL / DB_PRIMARY_URL, falling back to the discrete DB_* / PG_* fields (DB_HOST / PG_HOST, DB_PORT / PG_PORT, etc.).

DB_DIALECT=postgres DB_URL=postgres://app:app@localhost:5432/app voltro migrate

Migrating against DB_DIALECT=memory is rejected — there's nothing to migrate. In dev with the memory store, the in-memory store learns its shape from the schema declaration directly; switch to a SQL dialect (postgres, sqlite, mysql, mariadb, mssql, turso) to test the real migration path.

The declarative diff workflow — voltro db

The plan / apply / drift / squash machinery — diffing the declared schema against a live database, generating reviewable DDL, applying a pre-reviewed plan in production — lives under voltro db:

voltro db plan                            # diff declared schema vs live, color-coded
voltro db plan --json                     # machine-readable for CI / PR comments
voltro db plan --sql                      # raw DDL preview
voltro db plan --against <url>            # diff vs a REMOTE env via /_voltro/inspect/migrations
voltro db apply                           # execute the plan (dev only — refuses NODE_ENV=production)
voltro db apply --plan plan.json          # prod: apply a pre-reviewed plan from CI/CD
voltro db plans [--limit 20]              # plan history from _voltro_migration_plans
voltro db drift                           # alert if live diverged from the latest applied fingerprint
voltro db squash --before <iso-date>      # consolidate history into one snapshot
voltro db restore-snapshot <plan-id>      # restore soft-dropped columns from a plan

A separate file-based migration surface (the offline escape hatch) lives alongside it:

voltro db migrate                       # apply pending migration files
voltro db rollback [--to <id>]          # undo migrations
voltro db status                        # list applied / pending migrations
voltro db seed                          # run boot-lifecycle seeds against the configured store
voltro db seed --id <name>              # run one seed by id
voltro db seed --store memory           # explicit override; default = the app's configured store

voltro db seed defaults to the same store the app runs on (DB_DIALECTSTOREapp.config.ts store:postgres) — seeding to memory was a silent data-loss footgun, so you opt into it explicitly.

Production safety

For production deploys:

  1. Always apply schema BEFORE app version bumps. App N+1 expects schema N+1; app N should still tolerate schema N+1.
  2. Review the plan first. voltro db plan --json in CI, apply the reviewed plan with voltro db apply --plan plan.json.
  3. Check for drift. voltro db drift flags a live database that diverged from the last applied fingerprint.
  4. Verify backups before destructive changes. Restoring is the actually-tested rollback path.

For zero-downtime deploys with breaking schema changes:

  • Add new columns / tables → apply first, then deploy.
  • Remove columns → deploy code that doesn't read them, then apply the drop.
  • Rename → add new column + dual-write, deploy, drop old column later.

The framework doesn't enforce these — that's an SRE responsibility. See Migrations for the playbook.

See also