Migration CLI
Generate, run, and inspect database migrations.
The migration system tracks changes to your entity schema and generates SQL migrations.
Commands
Generate a migration
cms migration generate
Diffs the current code-defined schema against the latest snapshot and generates a new migration with SQL and a schema snapshot.
migrations/
0001_initial/
migration.sql
schema.json
0002_add_excerpt/
migration.sql
schema.json
Run pending migrations
cms migration run
Applies all pending migrations in order. Each migration runs in a transaction. Applied migrations are tracked in a _migrations table.
Check migration status
cms migration status
Lists all migrations with their status (applied or pending).
How it works
- Every migration has a
.sqlfile and aschema.jsonsnapshot generatediffs the current schema against the latest snapshot- Changes are classified by storage type: column (DDL), JSONB (data transform), or relation (FK/junction)
- You review the generated SQL and commit it
Migration operations
The differ emits typed operations:
Column operations
- add_column →
ALTER TABLE ... ADD COLUMN - drop_column →
ALTER TABLE ... DROP COLUMN - alter_column →
ALTER TABLE ... ALTER COLUMN
JSONB operations
JSONB fields don’t have DDL migrations. The system generates UPDATE statements that transform data in place:
-- Adding a field with default
UPDATE posts SET body = jsonb_set(body, '{subtitle}', '"default"')
WHERE NOT body @? '$.subtitle';
-- Removing a field
UPDATE posts SET body = body #- '{old_field}';
-- Renaming a field
UPDATE posts SET body = jsonb_set(
body #- '{oldName}',
'{newName}',
body #> '{oldName}'
)
WHERE body #> '{oldName}' IS NOT NULL;
Relation operations
- Add single relation →
ALTER TABLE ... ADD COLUMN x_id uuid REFERENCES ... - Add multiple relation →
CREATE TABLE entity_field (sourceId, targetId, position) - Remove → reverse of the above
Rename detection
The system can’t reliably detect renames (vs. add + remove). When ambiguous, it asks:
Is "body.old_field" → "body.subtitle" a rename? (y/n)
Schema snapshots
Each field is serialized with its resolved base type and storage info:
{
name: string;
type: string; // original type (e.g. "color-picker")
base: string; // resolved base type (e.g. "text")
storage: "column" | "jsonb" | "relation";
columnType?: string; // e.g. "varchar", "numeric"
unique?: boolean;
required?: boolean;
to?: string | string[];
multiple?: boolean;
shape?: Record<string, unknown>; // nested structure for JSONB
}
Previous
Config Reference