Skip to content
VibORM
Esc
navigateopen⌘Jpreview
On this page

Migrations

Manage database schema changes with VibORM migrations

VibORM provides two approaches for managing database schema changes:

  • Push — history-free live schema synchronization, ideal for development
  • Migrate — an authenticated, version-controlled estate, intended for production

Push compares your VibORM models with the live database. Migrate compares the desired schema snapshot with an authenticated parent state, publishes that transition, and separately verifies the live database when it applies it.

Choose Your Approach

Push Migrate
Use case Development, prototyping Production, team collaboration
Version control No history artifacts Yes, state manifests, snapshots, and reviewed SQL blobs committed to Git
Rollback Manual down() executes the stored down artifact, honouring each migration’s persisted rollback policy
Production 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

# Publish a new migration state
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 { createFsStorageWriter, createMigrationClient } from "viborm/migrations";
import { client } from "./client";

const migrations = createMigrationClient(client, {
  storage: createFsStorageWriter("./migrations"),
});

await migrations.generate({ name: "add-users" });
await migrations.apply();
await migrations.status();

const preview = await migrations.push({
  dryRun: true,
  resolve: async (change) => {
    if (change.type === "destructive") {
      return change.proceed();
    }
    if (change.type === "ambiguous") {
      return change.rename();
    }
    if (change.isNullable) {
      return change.useNull();
    }
    return change.reject();
  },
});

await migrations.push({ consent: preview.consent });

Capability-Typed Clients

The returned client exposes only operations that its storage can support:

Construction Surface
createMigrationClient(client) push, log
createMigrationClient(client, { storage: reader }) Adds estate inspection and application operations
createMigrationClient(client, { storage: writer }) Also adds generate and reset

This keeps a storage-less deployment script from compiling a call that can only fail with “storage required” at runtime.

How It Works

Both push and migrate follow the same core process:

┌─────────────────┐     ┌──────────────────┐     ┌─────────────────┐
│  VibORM Schema  │ ──▶ │   Schema Diff    │ ──▶ │  DDL Generation │
│   (your code)   │     │  (detect changes)│     │ (database SQL)  │
└─────────────────┘     └──────────────────┘     └─────────────────┘
  1. Serialize your VibORM models into a schema snapshot
  2. Compare against the current database state (push) or previous snapshot (migrate)
  3. Generate database-specific DDL using the appropriate migration driver
  4. Execute the DDL (push) or publish authenticated estate artifacts (migrate)

Atomicity

Migration atomicity is a DDL-specific guarantee and is not implied by support for client transactions. VibORM admits effectful work only when the concrete provider proves its lock, marker compare-and-swap, and execution boundary. MySQL DDL is the documented stepwise exception because each statement commits:

Driver push migrate apply
PGlite, pg, postgres.js, Bun SQL Transaction when every statement permits it Same guarantee
SQLite3, Bun SQLite One writer boundary; transactional where supported Same guarantee
MySQL2 Stepwise — a failed run can leave earlier statements applied Same limitation, with durable progress evidence
Neon HTTP, PlanetScale, D1, D1 HTTP, libSQL Effectful work refused; dry-run and read-only paths remain Effectful work refused until the provider boundary is proven

On the atomic paths, a successful migration applies every statement and a failed one applies none. The portable API exposes no transaction isolation options.

Next Steps

Was this page helpful?