Migrate Command
On this page 32
The buddy migrate command runs database migrations based on your model definitions, creating and updating database tables to match your application's data structure.
Basic Usage
# Run all pending migrations
buddy migrate
# Fresh migration (drop all tables and re-migrate)
buddy migrate:fresh
Command Syntax
buddy migrate [options]
buddy migrate:fresh [options]
Options for migrate
| Option | Description |
|---|---|
-d, --diff | Show the SQL that would be run without executing |
-p, --project [project] | Target a specific project |
-a, --auth | Also migrate auth tables (oauth_clients, oauth_access_tokens, etc.) |
--strict | Exit non-zero when the database has drifted from its migrations or models, or a migration was written by hand (for CI) |
--verbose | Enable verbose output |
Options for migrate:fresh
| Option | Description |
|---|---|
-d, --diff | Show the SQL that would be run without executing |
-p, --project [project] | Target a specific project |
-s, --seed | Run database seeders after migration |
-a, --auth | Also migrate auth tables |
--verbose | Enable verbose output |
Available Commands
Standard Migration
Run pending migrations:
buddy migrate
# or
buddy db:migrate
Fresh Migration
Drop all tables and re-run all migrations:
buddy migrate:fresh
# or
buddy db:fresh
DNS Migration
DNS is not migrated from here. buddy dns:pull prints a domain's live records
as a config/dns.ts block, buddy dns:diff shows what the file declares and the
zone is missing, and buddy dns:sync creates those records at the registrar.
Examples
Run Migrations
buddy migrate
Output:
buddy migrate
Migrated your local database.
Completed in 2.34s
Fresh Migration with Seeding
buddy migrate:fresh --seed
This drops all tables, runs migrations, and seeds the database with test data.
Preview Migration SQL
buddy migrate --diff
Shows what the next buddy migrate would do, without doing it: the committed
migration files the migrations table has not recorded yet, the model changes
that are not in a migration file yet, and any drift between the live database
and what its recorded migrations and models describe. It only says "your models
match the database" when all three are empty, and it exits non-zero if any of
them could not be read.
Migrate with Auth Tables
buddy migrate --auth
Includes authentication-related tables:
oauth_clientsoauth_access_tokensoauth_refresh_tokenspassword_resets
Full Fresh Migration with Everything
buddy migrate:fresh --seed --auth
Model-Based Migrations
Stacks uses a model-first approach to migrations. Your migrations are generated from model definitions:
// app/Models/User.ts
import { defineModel } from '@stacksjs/orm'
import { schema } from '@stacksjs/validation'
export default defineModel({
name: 'User',
table: 'users',
traits: {
useTimestamps: true, // created_at / updated_at
},
hasMany: ['Post'],
hasOne: ['Profile'],
attributes: {
name: {
required: true,
fillable: true,
validation: { rule: schema.string().max(255) },
},
email: {
required: true,
unique: true,
fillable: true,
validation: { rule: schema.string().email() },
},
password: {
required: true,
hidden: true, // excluded from JSON
validation: { rule: schema.string().min(8) },
},
},
} as const)
When you run buddy migrate, Stacks:
- Reads every model definition from
app/Models - Diffs them against the current database schema
- Emits SQL into
database/migrations/and runs what is pending
Migrations are emitted for one database. The SQL under
database/migrations/is generated for whichever dialect was configured at generation time, and the files shipped with a new project are SQLite. PointingDB_CONNECTIONat Postgres or MySQL without regenerating them will refuse to run, because the DDL is not portable. Runbuddy migrate:switch <driver>to see what a switch involves.The generator currently reads only
app/Models. If that directory is absent it produces nothing, so the framework's ownstorage/framework/defaults/app/Modelsare not picked up automatically.
Run buddy generate:migrations on its own to produce the SQL without applying
it, so you can review the file first.
Pre-flight checks
Before a single statement runs, buddy migrate audits the corpus twice and
refuses rather than failing halfway through, which would leave the schema
partly applied.
Is this SQL written for a different database? Dialect-exclusive syntax
(AUTOINCREMENT, SERIAL, AUTO_INCREMENT) is matched against the target.
This is what catches a SQLite corpus pointed at Postgres.
Does it use a feature this engine lacks? A separate question, and invisible
to the first check: FOREIGN KEY is perfectly valid MySQL, so a MySQL corpus
aimed at a distributed engine passes the syntax audit cleanly and then fails on
the first constraint. Distributed engines reject foreign keys, and sharded ones
also reject AUTO_INCREMENT.
Each failure names the affected files and what to do instead. If you know your corpus is correct, either check can be bypassed:
STACKS_ALLOW_DIALECT_MISMATCH=1 buddy migrate
STACKS_ALLOW_DDL_CONSTRAINT_VIOLATIONS=1 buddy migrate
Vitess uses its own online DDL
buddy migrate applies DDL over the connection. That is right for an
unsharded keyspace, but a sharded one expects schema changes through Vitess's
online DDL so they can roll out shard by shard. Generate the SQL and apply it
with vtctldclient ApplySchema. See
Scaling the Database.
Environment-Specific Migrations
Migrations respect the APP_ENV environment variable:
# Migrate local database (default)
APP_ENV=local buddy migrate
# Migrate staging database
APP_ENV=staging buddy migrate
# Migrate production database
APP_ENV=production buddy migrate
Database Configuration
Configure your database in config/database.ts:
export default {
default: 'sqlite',
connections: {
sqlite: {
driver: 'sqlite',
database: 'storage/database.sqlite',
},
mysql: {
driver: 'mysql',
host: process.env.DB_HOST,
port: process.env.DB_PORT,
database: process.env.DB_DATABASE,
username: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
},
},
}
Troubleshooting
No Models Found
Error: No models found. Please create models in app/Models or ensure framework defaults exist.
Solution: Create at least one model in app/Models/:
buddy make:model User
Migration Fails
If a migration fails:
- Check the error message for SQL issues
- Review your model definitions
- Run with
--verbosefor more details:
buddy migrate --verbose
Database Connection Failed
# Check your database configuration
cat config/database.ts
# Verify environment variables
echo $DB_HOST $DB_PORT $DB_DATABASE
Permission Denied
For SQLite:
chmod 664 storage/database.sqlite
For MySQL/PostgreSQL:
Verify your database user has the necessary permissions.
Schema Out of Sync
buddy migrate runs the files the migrations table does not list. A table
whose migration is listed but which is not in the database - dropped by hand,
lost in a restore - is therefore never recreated by a plain migrate. Both
migrate and migrate --diff report it, and neither calls the database up to
date while it is missing.
# Compare the migration files, the migrations table and the live schema
buddy migrate:status
# Rebuild a missing table without touching anything else
buddy migrate:status --reconcile --requeue-reverted
buddy migrate
--requeue-reverted un-records every migration that built the missing table,
so the next migrate replays its whole history in order. It only does so when
none of those files also changes a table that is still there, or writes data to
one; those tables are listed with the reason and left for you. A migration that
fails to apply is never silently skipped either: if any file the runner could
see is still unrecorded after a run, migrate fails and names it.
As a last resort, buddy migrate:fresh rebuilds everything and drops all data.
A Database That Drifted From Its Models
A database built from hand-written migrations can hold the right tables in the
wrong shape: indexes under names the models do not use, foreign keys the
models declare and the migrations left out, columns narrower than the model.
Regenerating the corpus does not repair that - CREATE ... IF NOT EXISTS is a
no-op against a name that is already taken - and neither does migrate.
# Rebuild database/migrations from the models (review the plan first)
buddy migrate:regenerate postgres --replace-unmarked --dry-run
buddy migrate:regenerate postgres --replace-unmarked --force
# Compare the live schema with the models, then repair what is safe
buddy migrate:align
buddy migrate:align --apply
# Record the regenerated files the schema now satisfies, then migrate
buddy migrate:status --reconcile
buddy migrate
migrate:regenerate replaces enum type files the new corpus re-declares, drops
of indexes it never builds, and DELETE/UPDATE-only data fixes (no-ops on a
database the corpus builds). It never removes a file that inserts rows
(INSERT ... VALUES, COPY): that is kept, and moved after the corpus when it
fills a table the corpus now creates. Against a database with history, it
retires the ledger rows of the files it removed and records each regenerated
file whose every table, index and type already exists - an index counts under
any name, including one Postgres truncated at 63 characters.
migrate:align (PostgreSQL) prints a plan by default. --apply runs, in one
transaction: renames of indexes that match a declared index in everything but
name, missing indexes, missing foreign keys, missing columns, lossless widening
(varchar(n) to text or a longer varchar; integer to bigint only with
--widen-integers, since it rewrites the table) and new enum labels. A foreign
key with orphan rows, or a unique index over duplicate values, is refused with
a count. Extra columns, narrowing, nullability and default changes are only
reported.
Hand-Written Migrations
Every migration is meant to be generated from app/Models. migrate:status
lists the files that carry no -- qb:generated marker, and
migrate --strict / migrate:status --strict fail on them. Package migrations
are not counted. Allow specific files - reference data no model can describe -
with database.safety.allowHandWrittenMigrations (a list of filenames, or of
names without the ordinal; true allows all), or for one run with
DB_ALLOW_HAND_WRITTEN_MIGRATIONS.
Best Practices
Development
# Use fresh migrations during development
buddy migrate:fresh --seed
Staging/Production
# Never use migrate:fresh in production
# Use regular migrate to preserve data
buddy migrate
Before Deployment
# Preview changes before applying
buddy migrate --diff
The snapshot is the baseline
buddy migrate works out what changed by diffing your models against a snapshot
of the last known schema:
storage/framework/database/model-snapshot.<dialect>.json
Without that file there is no baseline, so the same change is derived again on every run. Applying it fixes exactly one run, and the next one proposes it back.
That path sits inside the project, which is right for a checkout and wrong for a
release-directory deploy. Capistrano-style layouts put every deploy in a fresh
releases/<sha>, so the snapshot is never there, and a schema that is already
correct keeps reporting a pending change.
Point DB_SNAPSHOT_PATH at a directory that outlives the release:
# absolute, or relative to the release directory
DB_SNAPSHOT_PATH=/srv/app/shared/db
DB_SNAPSHOT_PATH=../../shared/db
The directory must be writable: migrate writes the new snapshot back after a successful run, and a read-only one leaves you where you started. When a destructive change is proposed and no snapshot exists, migrate now names the directory it looked in rather than leaving you to guess.
Related Commands
- buddy seed - Seed database with test data
- buddy generate:migrations - Generate migrations from your models
- buddy make:model - Create a new model