Skip to content
RELKIT
Esc
↑↓navigate↵open⌘Jpreview
On this page

Migrations

Use Drizzle Kit to generate and apply migrations from TypeScript tables, and evolve persistent data safely.

A migration applies a reviewed schema change to a database. With Drizzle, generate migrations from your TypeScript tables using Drizzle Kit. You do not write table-creation statements in defineDrizzleService or maintain a second handwritten copy of your schema.

RELKIT’s check, build, and dev commands do not apply migrations. Migrate the database before starting code that depends on its new shape.

Configure Drizzle Kit

Add this application-root configuration, adjusting the schema path for your app:

/** Points native Drizzle tooling at grouped model tables and rejects empty or in-memory migration targets. */
import { defineConfig } from "drizzle-kit";

const path = process.env.DATABASE_PATH;
if (path === undefined || path.trim() === "" || path === ":memory:") {
  throw new Error("Set DATABASE_PATH to the persistent SQLite file you intend to migrate");
}

export default defineConfig({
  schema: "./src/services/database/schema/index.ts",
  dialect: "sqlite",
  out: "./src/services/database/migrations",
  dbCredentials: { url: path },
});

schema points to the same table exports used by the service. dialect describes the database, out owns the generated migration history, and dbCredentials selects the migration target. The configuration deliberately requires an explicit persistent file path. A separate process cannot migrate the in-memory connection later opened by your application.

The example’s package scripts use bun --bun drizzle-kit generate and relkit db migrate. Shared migration execution lives in installed RELKIT tooling; SQLite uses the Bun-native driver without a copied runner. PostgreSQL/MySQL delegate to the application’s installed Drizzle Kit under Bun.

Keep SQL and required metadata tracked in src/services/database/migrations, beside the owning service and schema. Auth and business tables share this one ordered history; .relkit/ contains ignored runtime/build output, not migration source.

Generate, review, and apply

For a new app and a new local file, run these commands from the application root:

export DATABASE_PATH=./data-model.sqlite
bun --bun drizzle-kit generate --name=initial
bun run db:migrate
bun run check
bun run typecheck
bun run dev

If you are running the repository example, its initial migration is already generated and checked in; start with bun run db:migrate after setting DATABASE_PATH. Use the same path for the migration command and the running server. Run from the application root containing package.json and relkit.config.ts. Select another config with bun run relkit db migrate --config configs/database.ts; the default is root drizzle.config.ts. Its connection, output and supported log settings are authoritative. Relative database/output paths still use the application root even for nested or absolute config selections.

Bun loads the application’s normal dotenv files, with inherited variables taking precedence; .env.example is never loaded. Keep PostgreSQL/MySQL configuration deterministic because delegated Kit loads it again. Missing tooling is an explicit installation error; the migration command never downloads packages implicitly.

generate compares TypeScript schema definitions with migration snapshots and creates migration artifacts; it does not modify your database. Review the generated changes before applying them. migrate applies pending migrations and records their history. See the official Drizzle Kit generate guide and migrate guide for the tooling contract.

Commit the generated migration directory, including snapshots, alongside the schema change. Let Kit own the output layout. Do not hand-edit generated snapshots or rewrite migrations already applied to shared environments. Native statements and advanced manual migrations belong in the Drizzle documentation, not the normal service setup.

Change an existing model

Edit the relevant Drizzle table, then generate a new named migration:

export DATABASE_PATH=./data-model.sqlite
bun --bun drizzle-kit generate --name=users-change

Review the diff and test it on an isolated copy of the old schema with representative data. After verification, apply the pending migrations to the intended target with bun run db:migrate (or bun run relkit db migrate if you have not added the script).

Keep model changes, generated artifacts, and affected application contracts in the same change. Renames need particular attention so they are not mistaken for deleting and recreating data.

A database created by older demo bootstrap code or by another tool has no matching Drizzle migration history. Do not blindly apply the initial migration to it. Establish a baseline using a workflow appropriate to your existing database and Drizzle version before continuing. Do not use a reset or force flag to make an existing database fit the tutorial.

Deploy without racing application replicas

Run migrations as one coordinated deployment step, with deliberate credentials and the required Drizzle tooling/configuration/artifacts available. Do not run schema changes in a request handler or have every server replica migrate at startup.

For changes used by running versions, prefer an additive field or table, a controlled backfill, a compatible application rollout, and only then removal of the old shape. Non-null fields may need defaults or a backfill; unique constraints may fail on existing duplicates.

Before destructive changes, verify backups, restoration, the exact database target, and lock/downtime expectations. A rollback cannot necessarily recover discarded data. Keep seeding separate from migrations and normal server startup.

For PostgreSQL or MySQL, use matching table definitions, Kit dialect/connection settings, and the appropriate driver. This executable workflow verifies Bun SQLite; it is not a test of other dialects.

Verify migrations separately from queries

From the data-model example root:

bun test tests/migrations.test.ts
bun test tests/data-model.test.ts
bun run check
bun run typecheck

The migration test invokes shared RELKIT tooling against a temporary file, writes data through Drizzle, and runs migrations again to verify that the data survives. Application tests apply the generated migration to their own isolated connection. As history grows, add upgrade tests from deployed versions, not just tests of an empty database.

relkit db migrate --json emits one secret-safe result after cleanup: success identifies command, dialect and config/history paths, without inventing an applied count. Usage/configuration errors exit 2, operational failures 1, interruption 130, and success 0. Missing, empty or incompatible history fails before opening the target database and points to generation/compatibility checks. Failed SQL is not automatically retried; partial DDL rollback is not guaranteed.

See Testing for the executable tests and Better Auth before adding authentication tables.

Reference and source