Migrations
Manage database schema changes with VibORM migrations
VibORM provides two approaches for managing database schema changes:
- Push - Direct schema synchronization, ideal for development
- Migrate - File-based migrations with version control, ideal for production
Both approaches work by comparing your VibORM schema definitions against the current database state and generating the necessary DDL to synchronize them.
Choose Your Approach
| Push | Migrate | |
|---|---|---|
| Use case | Development, prototyping | Production, team collaboration |
| Version control | No migration files | Yes, SQL files committed to git |
| Rollback | Manual | Supported via down migrations |
| CI/CD | Not recommended | Fully supported |
CLI vs API
VibORM migrations can be used via the CLI or programmatically via the Migration Client API.
CLI
The CLI is the recommended way to manage migrations during development:
# Push schema directly to database
npx viborm push
# Generate a new migration file
npx viborm migrate generate --name add-users
# Apply pending migrations
npx viborm migrate apply
# Check migration status
npx viborm migrate status
Migration Client API
For programmatic control (scripts, CI/CD, custom tooling), use the migration client:
import { createMigrationClient } from "viborm/migrations";
import { createFsStorageDriver } from "viborm/migrations/storage/fs";
import { client } from "./client";
const migrations = createMigrationClient(client, {
storageDriver: createFsStorageDriver("./migrations"),
});
// File-based migration operations (require storage driver)
await migrations.generate({ name: "add-users" });
await migrations.apply();
await migrations.status();
// Push works with or without storage driver
await migrations.push({
resolve: async (change) => {
// Handle destructive or ambiguous changes
if (change.type === "destructive") {
return "proceed"; // or "reject"
}
return "rename"; // or "addAndDrop" or "reject"
},
});
Migration Client Options
interface MigrationClientOptions {
/** Storage driver for migration files (required for file-based operations) */
storageDriver?: MigrationStorageDriver;
/** Migration tracking table name (default: _viborm_migrations) */
tableName?: string;
}
How It Works
Both push and migrate follow the same core process:
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ VibORM Schema │ ──▶ │ Schema Diff │ ──▶ │ DDL Generation │
│ (your code) │ │ (detect changes)│ │ (database SQL) │
└─────────────────┘ └──────────────────┘ └─────────────────┘
- Serialize your VibORM models into a schema snapshot
- Compare against the current database state (push) or previous snapshot (migrate)
- Generate database-specific DDL using the appropriate migration driver
- Execute the DDL (push) or write to a migration file (migrate)
Atomicity
Migration atomicity is a DDL-specific guarantee and is not implied by support for client transactions. VibORM uses a real transaction or atomic batch where the database can honor it. D1 binding and Neon apply paths reject when that substrate is absent; MySQL-family DDL is the documented limited exception because its statements implicitly commit:
| Driver | push |
migrate apply |
|---|---|---|
| PostgreSQL, SQLite family | Transaction (all-or-nothing) | Transaction (all-or-nothing) |
| MySQL, PlanetScale | Limited — DDL statements implicitly commit, so a failed run can leave earlier statements applied | Same limitation |
| D1 bindings, Neon HTTP | Atomic batch | Rejects — requires callback transactions, which these drivers don’t support |
On the atomic paths, a successful migration applies every statement and a failed one applies none. The portable API exposes no transaction isolation options.