Migrations
Drizzle.Schema puts drizzle-kit generate inside the deploy
graph: on each deploy it diffs your schema module against the latest
checked-in snapshot, writes any pending migration SQL to out, and
the database resource downstream applies it via migrations — in
that order, because schema.out is an Output the database depends
on.
const schema = yield* Drizzle.Schema("app-schema", { schema: "./src/schema.ts", out: "./migrations", dialect: "sqlite",});
const db = yield* Cloudflare.D1.Database("app-db", { migrations: schema,});What a deploy actually does
Section titled “What a deploy actually does”- No drift — the schema matches the latest snapshot: nothing is generated, the resource is a noop, and nothing cascades into the database resource.
- Unambiguous drift — a new table, a new column: the migration SQL is generated automatically and applied in the same deploy.
- Ambiguous drift — a change drizzle-kit cannot decide alone
(did you rename
emailtocontact, or drop one column and add another? a change that would lose data): it needs your answer.- In a terminal,
alchemy deploysurfaces drizzle-kit’s interactive prompts and you answer inline. - Non-interactively (CI,
--yespipelines), the deploy fails with drizzle-kit’s report and instructions: rundrizzle-kit generateyourself, answer the prompts, commit the generated files, and redeploy. The generated SQL is applied to a real database in the same deploy — alchemy never guesses on a destructive choice.
- In a terminal,
Generated files land under out as ordered migration directories
containing migration.sql and snapshot.json. Commit them — they
are source code, shared with every other environment that deploys
the same app.
Or: run drizzle-kit yourself
Section titled “Or: run drizzle-kit yourself”Drizzle.Schema is optional. If you prefer to run
drizzle-kit generate manually and commit the output, point
migrations at the checked-in directory — the database resource
only sees .sql files:
const db = yield* Cloudflare.D1.Database("app-db", { migrations: "./drizzle", // drizzle-kit's default out});Durable Object migrations
Section titled “Durable Object migrations”A Durable Object’s SQLite database is per-instance, so its
migrations run inside the object at runtime rather than from the
deploy graph (see drizzle’s
Durable Objects guide).
Generate them with drizzle-kit’s durable-sqlite driver:
import { defineConfig } from "drizzle-kit";
export default defineConfig({ dialect: "sqlite", driver: "durable-sqlite", schema: "./src/schema.ts", out: "./drizzle",});Define the schema — tables plus
defineRelations for
typed relational queries:
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 posts = sqliteTable("posts", { id: integer("id").primaryKey({ autoIncrement: true }), userId: integer("user_id").notNull().references(() => users.id), title: text("title").notNull(),});
export const relations = defineRelations({ users, posts }, (t) => ({ users: { posts: t.many.posts() }, posts: { author: t.one.users({ from: t.posts.userId, to: t.users.id }) },}));drizzle-kit generate
writes the migration directories plus a migrations.js bundle that
imports each migration’s .sql file. The alchemy bundler loads bare
.sql imports as text modules (the same way it handles .txt and
.html), so the generated bundle imports directly into your Durable
Object. Commit it and hand it to Drizzle.DurableObject, along with
the relations:
import * as Drizzle from "alchemy/Drizzle/Cloudflare";import migrations from "./drizzle/migrations.js";import { posts, relations, users } from "./schema.ts";
export class Users extends Cloudflare.DurableObject<Users>()( "Users", Effect.gen(function* () { return Effect.gen(function* () { const db = yield* Drizzle.DurableObject({ migrations, relations });
return { addUser: (name: string) => db.insert(users).values({ name }), listUsers: () => db.select().from(users), listUsersWithPosts: () => db.query.users.findMany({ with: { posts: true } }), }; }); }),) {}Drizzle.DurableObject (from alchemy/Drizzle/Cloudflare) opens
drizzle over the instance’s own SQLite storage using the
drizzle-orm/effect-sqlite-do integration — the
same effect-native query surface as Drizzle.D1
and Drizzle.Postgres. It lives in the
object’s inner Effect — the instance init, which runs only in the
deployed object, before any request reaches its methods. The config
passes the driver’s options through — the relations make
db.query.users.findMany({ with: { posts: true } }) fully typed.
Every query carries a typed error channel, so failures are handled
per operation with Effect.catchTag:
db.insert(users).values({ name }).pipe( Effect.catchTag("EffectDrizzleQueryError", (error) => Effect.succeed(undefined), ),);EffectDrizzleQueryError carries the query, its params, and the
underlying cause. Transactions add the effect-sql SqlError to the
union, and the migrator can fail with MigratorInitError.
Migrating from drizzle-kit
Section titled “Migrating from drizzle-kit”Already ran drizzle-kit migrate against the database before
Alchemy? No baselining needed: on the first deploy Alchemy copies
the applied history out of __drizzle_migrations (the drizzle
schema on Postgres) into its own __alchemy_migrations table —
hashes carry over verbatim, since both record sha256 of
migration.sql — and only genuinely pending migrations run. The
drizzle table is left frozen, never written or dropped.
It’s a one-way move: from then on Alchemy owns the bookkeeping, so
retire drizzle-kit migrate from your workflow (drizzle-kit generate is subsumed by Drizzle.Schema). See
adopting an existing database
for the shared mechanics and guard rails.
Destroy never deletes migrations
Section titled “Destroy never deletes migrations”The provider’s delete operation is a deliberate no-op. Removing
the resource (or destroying the stack) never wipes the migrations
directory — migration files are source code you commit, not cloud
state to tear down.
Dialects
Section titled “Dialects”dialect selects drizzle-kit’s target: "postgres" (the default)
for Neon / PlanetScale / Fly / Hyperdrive-fronted
Postgres, "mysql" for
PlanetScale MySQL, "sqlite" for
D1.
Where next
Section titled “Where next”- Postgres / MySQL / D1 — the full schema-to-queries flow per database.
- Effect SQL migrations — the same
migrationsapplication without an ORM. - Schema API reference