Skip to main content
Vertz handles migrations by diffing your TypeScript schema against the database. During development, migrations run automatically when you save a schema file — no commands needed. For production, you generate migration files and apply them explicitly.

Getting started

1

Define your schema

Create a .schema.ts file with your table definitions using the d builder. See the schema guide for details.
2

Start the dev server

Run vertz dev. The dev server watches your schema files and applies migrations automatically when you save.
3

Generate migration files before deploying

When you’re ready to deploy, run vertz db migrate to create a migration file, then vertz db deploy in production to apply it.

Command comparison

CommandCreates migration file?Applies to DB?Use when
vertz devNoYes (auto)Local development — schema changes apply on save
vertz db pushNoYesQuick one-off schema sync without a migration file
vertz db migrateYesYesGenerating a reviewable migration file for production
vertz db deployNoYes (pending only)Production deployments — applies committed migration files
vertz db pullNoNoGenerate a schema file from an existing database
autoMigrate()NoYesProgrammatic dev setup (test suites, scripts). SQLite only.
  • vertz dev — the default for local development. No commands to remember.
  • vertz db push — useful when you want to apply changes outside the dev server without generating a migration file (e.g., CI test databases).
  • vertz db migrate — generates a numbered .sql file in your migrations directory and applies it. Commit this file to version control.
  • vertz db deploy — reads migration files from disk and applies any that haven’t been applied yet. Never generates new files. Safe for production.
  • vertz db pull — connects to an existing database and generates a TypeScript schema file. See the introspection guide.
  • autoMigrate() — a programmatic API for test setup or custom scripts. It diffs the schema snapshot and applies changes directly. Currently supports SQLite only — Postgres users should use vertz db push or migrateDev() instead.

File structure

After running vertz db migrate, your project will have:
my-app/
├── migrations/
│   ├── 0000_initial.sql              # First migration (raw SQL)
│   ├── 0001_add-priority-to-tasks.sql
│   ├── _journal.json                 # Migration metadata & checksums
│   └── _snapshot.json                # Current schema snapshot
├── src/
│   └── tasks.schema.ts               # Your schema definitions
└── vertz.config.ts
FilePurpose
migrations/*.sqlNumbered migration files containing DDL statements. Commit these.
migrations/_journal.jsonTracks which migrations exist, their checksums, and creation timestamps. Commit this.
migrations/_snapshot.jsonThe current schema state used for diffing. Commit this.
The migrations directory defaults to ./migrations/ and can be changed via migrationsDir in vertz.config.ts.

Development workflow

During vertz dev, schema changes are detected and applied automatically:
# Start the dev server
vertz dev

# Edit a schema file — migration runs automatically:
# [DB] Schema change detected: tasks.schema.ts
# [DB] Applied: added column "priority" to "tasks"
Change your schema, save the file, keep building. The dev server:
  1. Detects .schema.ts file changes
  2. Diffs the current schema against the last snapshot
  3. Generates and applies the necessary SQL
  4. Warns on destructive changes (table or column drops)
  5. Updates the snapshot
No migration files are written during development.

Manual commands

If you need to run migrations outside the dev server:
# Generate and apply migration from schema diff
vertz db migrate

# Push schema changes directly (no migration file)
vertz db push

# Drop everything and re-apply from scratch
vertz db reset
vertz db reset destroys all data. Use only in development.

Production workflow

For production deployments, use explicit migration files:
# Generate a migration file from schema changes
vertz db migrate

# Apply pending migration files (production-safe)
vertz db deploy
vertz db deploy only applies existing migration files — it never generates new ones. This ensures production databases only receive reviewed, committed migrations.

Check migration status

vertz db status
Shows which migrations have been applied and which are pending. Also detects schema drift — differences between the expected schema and the actual database state.

Baseline

For existing databases, create a baseline migration that represents the current state:
vertz db baseline
This marks the current database state as the starting point for future migrations — no SQL is applied.

Programmatic API

For custom tooling or test setup:
import { migrateDev, migrateDeploy, migrateStatus } from 'vertz/db';

// Development: generate and apply migration
await migrateDev({
  queryFn: db.queryFn,
  currentSnapshot: db.snapshot,
  previousSnapshot: loadFromFile(),
  migrationsDir: './migrations',
});

// Production: apply pending migration files
await migrateDeploy({
  queryFn: db.queryFn,
  migrationsDir: './migrations',
});

// Check status
const status = await migrateStatus({
  queryFn: db.queryFn,
  migrationsDir: './migrations',
});

RLS policy migrations

If you use RLS policy generation from the codegen system, the migration pipeline can include RLS changes alongside schema changes. When you pass rlsPolicies to migrateDev(), it diffs the current RLS state against the previous snapshot and appends the necessary SQL:
import { migrateDev } from 'vertz/db';

await migrateDev({
  queryFn: db.queryFn,
  currentSnapshot: db.snapshot,
  previousSnapshot: loadFromFile(),
  migrationsDir: './migrations',
  rlsPolicies: codegenOutput.rlsPolicies, // from codegen
});
The generated migration includes:
  1. Schema DDL changes (table/column modifications)
  2. ALTER TABLE ... ENABLE ROW LEVEL SECURITY for new RLS tables
  3. DROP POLICY for removed or changed policies
  4. CREATE POLICY for new or changed policies
  5. ALTER TABLE ... DISABLE ROW LEVEL SECURITY for tables that no longer have policies
The snapshot tracks RLS state, so subsequent migrations only include incremental changes.

Setting session variables

RLS policies reference PostgreSQL session variables (app.user_id, app.tenant_id) via current_setting(). These must be set per-request within a transaction using SET LOCAL:
BEGIN;
SET LOCAL app.user_id = '<user-uuid>';
SET LOCAL app.tenant_id = '<tenant-uuid>';
-- All queries here are filtered by RLS policies
SELECT * FROM tasks;
COMMIT;
SET LOCAL scopes the variables to the current transaction — they’re automatically cleared on commit or rollback. The framework’s entity CRUD pipeline sets these automatically when RLS policies are present.

Troubleshooting

Snapshot out of sync

Symptom: vertz db migrate generates unexpected changes (re-adding columns that already exist, or dropping columns you didn’t remove). Cause: The _snapshot.json file doesn’t match the actual database state. This can happen if you applied schema changes manually or edited the database outside the migration system. Fix:
  1. Run vertz db status to see what the system thinks the current state is.
  2. If the database is correct and the snapshot is wrong, run vertz db push to sync the snapshot to match your current schema, then run vertz db migrate to generate a clean migration.
  3. If the database is wrong, run vertz db reset (development only) to rebuild from migration files.

Migration fails to apply

Symptom: vertz db deploy or vertz db migrate fails with a SQL error. Fix:
  1. Read the error — it usually points to a specific SQL statement (e.g., column already exists, constraint violation).
  2. If the migration was partially applied, check vertz db status to see which migrations succeeded.
  3. Fix the failing migration SQL file manually, then re-run vertz db deploy.
  4. If the database is in an inconsistent state in development, vertz db reset will drop everything and re-apply all migrations from scratch.
vertz db reset drops all tables and data. Never use in production.

Concurrent schema edits

Symptom: Two developers edit the schema at the same time and both run vertz db migrate. The second migration may conflict with the first. Fix:
  1. The journal system detects sequence number collisions. If two migrations share the same number, vertz db migrate will warn you.
  2. Pull the latest migrations from version control, resolve any conflicts in the SQL files and _journal.json, then re-run vertz db migrate.

autoMigrate not available for Postgres

autoMigrate() only supports SQLite. If you need programmatic migration for Postgres in test setup or scripts, use migrateDev() or push() from vertz/db instead:
import { push } from 'vertz/db';

await push({
  queryFn: db.queryFn,
  currentSnapshot: db.snapshot,
  previousSnapshot: loadFromFile(),
  dialect: 'postgres',
});