Skip to content

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”
Terminal window
pnpm exec drizzle-kit generate

Answer any rename prompts and review the generated SQL before applying it. generate writes migration files; migrate applies them to a database.

Terminal window
git add src/schema.ts drizzle
git commit -m "Add schema migration"

Include both the SQL and snapshots:

drizzle/
└── 20260919000000_create_users/
├── migration.sql
└── snapshot.json

Point the database resource at the checked-in directory:

const db = yield* Cloudflare.D1.Database("app-db", {
migrations: "./drizzle",
});
Terminal window
pnpm alchemy deploy

This 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.

Capture SQL during construction; apply it when each object activates:

Cloudflare.SqlMigrations("./drizzle") → read files into the Worker bundle
Drizzle.DurableObject({ migrations, ... }) → migrate this object's SQLite database

Deployment does not eagerly migrate every object in the namespace.

drizzle.config.ts
import { defineConfig } from "drizzle-kit";
export default defineConfig({
dialect: "sqlite",
schema: "./src/schema.ts",
out: "./drizzle",
});

Define a table and its query relations:

src/schema.ts
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:

Terminal window
pnpm exec drizzle-kit generate
git add src/schema.ts drizzle
git 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.

src/Users.ts
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:

Terminal window
cd my-app # Contains ./drizzle.
pnpm alchemy dev

After SQL edits, restart alchemy dev or redeploy. SQL directories are not watched.

// Default: __alchemy_migrations
const migrations = yield* Cloudflare.SqlMigrations("./drizzle");
// Custom table
const migrations = yield* Cloudflare.SqlMigrations({
dir: "./drizzle",
table: "app_migrations",
});

Each pending file and its history row commit in one native SQLite transaction:

0001_create_users → SQL + history row commit
0002_add_email → SQL fails; this file rolls back; activation fails
next activation → skip 0001; retry 0002

Keep 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.

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 files

For the legacy meta/_journal.json layout, upgrade before planning:

Terminal window
pnpm exec drizzle-kit up
# Review the upgraded SQL and snapshots.
git add drizzle
git commit -m "Upgrade Drizzle migration layout"

See the complete Worker example for named-object POST and GET routes.

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.

Removing Drizzle.Schema or destroying the stack leaves the checked-in migration directory intact. Database deletion still follows the database resource’s lifecycle.

dialect Database guide
"postgres" (default) Neon, PlanetScale, Fly, Hyperdrive
"mysql" PlanetScale MySQL
"sqlite" D1, Durable Objects