Migrations
Migration files belong in Git alongside your schema. Generate and review them when the schema changes, commit them, then deploy.
Generate migrations when the schema changes
Section titled “Generate migrations when the schema changes”pnpm exec drizzle-kit generateAnswer any rename prompts and review the generated SQL before applying it.
generate writes migration files; migrate applies them to a database.
Commit the schema and migrations
Section titled “Commit the schema and migrations”git add src/schema.ts drizzlegit commit -m "Add schema migration"Include both the SQL and snapshots:
drizzle/└── 20260919000000_create_users/ ├── migration.sql └── snapshot.jsonDeploy the committed migrations
Section titled “Deploy the committed migrations”Point the database resource at the checked-in directory:
const db = yield* Cloudflare.D1.Database("app-db", { migrations: "./drizzle",});pnpm alchemy deployThis resource applies pending SQL from ./drizzle. CI should deploy the
committed files, not generate a new migration history.
For optional schema-generation automation, see the
Drizzle.Schema reference. The workflow above
keeps generation and review separate from deployment.
Durable Object migrations
Section titled “Durable Object migrations”Capture SQL during construction; apply it when each object activates:
Cloudflare.SqlMigrations("./drizzle") → read files into the Worker bundleDrizzle.DurableObject({ migrations, ... }) → migrate this object's SQLite databaseDeployment does not eagerly migrate every object in the namespace.
Generate and commit the SQL
Section titled “Generate and commit the SQL”import { defineConfig } from "drizzle-kit";
export default defineConfig({ dialect: "sqlite", schema: "./src/schema.ts", out: "./drizzle",});Define a table and its query relations:
import { defineRelations } from "drizzle-orm";import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
export const users = sqliteTable("users", { id: integer("id").primaryKey({ autoIncrement: true }), name: text("name").notNull(),});
export const relations = defineRelations({ users });Generate locally, review the SQL, and commit the schema and migrations together:
pnpm exec drizzle-kit generategit add src/schema.ts drizzlegit commit -m "Create users table"CI uses these committed files when deploying.
SqlMigrations reads existing files. It does not run drizzle-kit or wait for Drizzle.Schema.
Load the directory in the outer Effect
Section titled “Load the directory in the outer Effect”import * as Cloudflare from "alchemy/Cloudflare";import * as Drizzle from "alchemy/Drizzle/Cloudflare";import * as Effect from "effect/Effect";import { relations, users } from "./schema.ts";
export class Users extends Cloudflare.DurableObject<Users>()( "Users", Effect.gen(function* () { // Construction: capture SQL without importing .sql or migrations.js. const migrations = yield* Cloudflare.SqlMigrations("./drizzle");
return Effect.gen(function* () { // Activation: apply pending SQL before exposing methods. const db = yield* Drizzle.DurableObject({ migrations, relations });
return { addUser: (name: string) => db.insert(users).values({ name }).returning(), listUsers: () => db.query.users.findMany(), }; }); }),) {}Paths are relative to the Alchemy command’s working directory, not Users.ts:
cd my-app # Contains ./drizzle.pnpm alchemy devAfter SQL edits, restart alchemy dev or redeploy. SQL directories are not watched.
Choose a history table
Section titled “Choose a history table”// Default: __alchemy_migrationsconst migrations = yield* Cloudflare.SqlMigrations("./drizzle");// Custom tableconst migrations = yield* Cloudflare.SqlMigrations({ dir: "./drizzle", table: "app_migrations",});Apply migrations before activation
Section titled “Apply migrations before activation”Each pending file and its history row commit in one native SQLite transaction:
0001_create_users → SQL + history row commit0002_add_email → SQL fails; this file rolls back; activation failsnext activation → skip 0001; retry 0002Keep applied files unchanged. Migration failures are initialization defects; the object serves no requests until activation succeeds. Query errors remain typed (EffectDrizzleQueryError; transactions can also fail with SqlError).
Without Drizzle, use the same engine in the inner Effect:
yield* migrations.apply().pipe(Effect.orDie);See the raw SQL example for the complete object.
Existing Drizzle migrations
Section titled “Existing Drizzle migrations”Generated migrations.js inputs still work with Drizzle’s migrator. To switch to Alchemy’s engine, replace the import with a construction-time capture:
import migrations from "../drizzle/migrations.js";
Effect.gen(function* () { const migrations = yield* Cloudflare.SqlMigrations("./drizzle"); return Effect.gen(function* () { const db = yield* Drizzle.DurableObject({ migrations, relations });__drizzle_migrations → copy matching history → __alchemy_migrations leave old table frozen; apply only pending filesFor the legacy meta/_journal.json layout, upgrade before planning:
pnpm exec drizzle-kit up# Review the upgraded SQL and snapshots.git add drizzlegit commit -m "Upgrade Drizzle migration layout"See the complete Worker example for named-object POST and GET routes.
Existing migration history
Section titled “Existing migration history”If you explicitly choose Alchemy to manage migrations for an existing database, see adopting an existing database for the one-way history conversion and compatibility checks. Adoption is a separate decision from generating and committing migration files.
Destroy never deletes migrations
Section titled “Destroy never deletes migrations”Removing Drizzle.Schema or destroying the stack leaves the checked-in migration directory intact. Database deletion still follows the database resource’s lifecycle.
Dialects
Section titled “Dialects”dialect |
Database guide |
|---|---|
"postgres" (default) |
Neon, PlanetScale, Fly, Hyperdrive |
"mysql" |
PlanetScale MySQL |
"sqlite" |
D1, Durable Objects |