1---
2title: Migrations
3description: Commit ordered SQL files and apply them on database deploy or Durable Object activation, tracked in one Alchemy-owned table.
4---
5
6Migrations without an ORM: commit `.sql` files to a directory and
7wire it into the database resource with `migrations`. Each deploy
8applies the pending files — there is nothing else to run. For
9[Durable Objects](#durable-object-migrations), load the files during
10construction and apply them per instance at activation instead.
11
12## Write the migrations
13
14```sql
15-- migrations/0001_init.sql
16CREATE TABLE users (
17 id INTEGER PRIMARY KEY AUTOINCREMENT,
18 name TEXT NOT NULL,
19 email TEXT NOT NULL UNIQUE
20);
21```
22
23```sql
24-- migrations/0002_posts.sql
25CREATE TABLE posts (
26 id INTEGER PRIMARY KEY AUTOINCREMENT,
27 user_id INTEGER NOT NULL REFERENCES users(id),
28 title TEXT NOT NULL
29);
30```
31
32## Wire the directory in
33
34```typescript
35const db = yield* Cloudflare.D1.Database("app-db", {
36 migrations: "./migrations",
37});
38```
39
40The same prop exists on `Neon.Project`, `Neon.Branch`,
41`Fly.Postgres`, and the PlanetScale databases and branches.
42
43## How files are applied
44
45- The directory is scanned recursively for `.sql` files.
46- Files are ordered by numeric prefix (`0001_init.sql`,
47 `0002_posts.sql`), then by name.
48- Each file is content-hashed and applied files are recorded, so a
49 deploy only runs what's new — an unchanged directory is a noop.
50- Application is ordered and stops on the first failure; the failed
51 file is retried on the next deploy.
52
53## The tracking table
54
55Applied migrations are recorded in one Alchemy-owned table,
56`__alchemy_migrations`, on every database:
57
58```sql
59CREATE TABLE IF NOT EXISTS "__alchemy_migrations" (
60 id SERIAL PRIMARY KEY, -- INTEGER on SQLite
61 hash text NOT NULL, -- sha256 of the file
62 created_at bigint, -- millis from a timestamp prefix, if any
63 name text, -- the applied-detection key
64 applied_at timestamp with time zone DEFAULT now()
65);
66```
67
68Detection is by `name`, so renaming an applied file is a schema
69change, not a cosmetic one.
70
71To put the bookkeeping in a differently-named table, use the object
72form of the prop:
73
74```typescript
75const db = yield* Cloudflare.D1.Database("app-db", {
76 migrations: {
77 dir: "./migrations",
78 table: "my_migrations", // default: "__alchemy_migrations"
79 },
80});
81```
82
83## Adopting an existing database
84
85A database previously migrated with **drizzle-kit**, **Prisma**, or
86**wrangler** needs no baselining. On the first deploy, Alchemy finds
87the old tool's tracking table — `__drizzle_migrations` (in the
88`drizzle` schema on Postgres), `_prisma_migrations`, or
89`d1_migrations` — copies its applied history into
90`__alchemy_migrations` once, and leaves the old table **frozen**:
91never written, never dropped. Only migrations the old tool hadn't
92applied yet actually run.
93
94This is a one-way move. After adoption, Alchemy's table is the only
95bookkeeping — the old tool's table stops reflecting reality, so stop
96running its migrate command.
97
98Two guard rails:
99
100- Every recorded row must match a local migration file; an orphaned
101 row fails the deploy with `MigrationHistoryConflictError` — it
102 means migrations were applied that your checkout doesn't have.
103- A **failed** Prisma migration (`finished_at` NULL) blocks adoption
104 until repaired with `prisma migrate resolve`; rolled-back rows are
105 skipped.
106
107Databases migrated by older versions of Alchemy are upgraded to the
108current table shape in place, at whatever table name their state
109recorded — no action needed.
110
111Each database documents its own mechanics — transactionality and
112quirks:
113
114| Database | Mechanics |
115| --- | --- |
116| Cloudflare D1 | [D1 migrations](/cloudflare/data/d1#migrations) — batched application (no transactions over HTTP) |
117| Neon | [Neon migrations](/neon/data/migrations) — applied transactionally on the branch |
118| PlanetScale | [PlanetScale migrations](/planetscale/data/migrations) — the same contract on Postgres and MySQL branches |
119| Fly Managed Postgres | [Fly Postgres](/fly/data/postgres#migrations) — applied over the direct URI |
120
121## Generated migrations target the same contract
122
123Generate and commit files with drizzle-kit before deployment:
124
125```sh
126pnpm exec drizzle-kit generate
127git add src/schema.ts drizzle
128git commit -m "Add schema migration"
129```
130
131The same directory input accepts handwritten SQL or drizzle-kit's
132`<timestamp>_<name>/migration.sql` layout; see [Drizzle migrations](/sql/drizzle/migrations).
133Optional generation automation is documented separately in the
134[`Drizzle.Schema` reference](/providers/drizzle/schema).
135
136## Durable Object migrations
137
138A Durable Object's SQLite database belongs to one named instance,
139not to the Worker deployment. `Cloudflare.SqlMigrations` reads and
140normalizes a migration directory during construction/planning, then
141embeds its records in the Worker JavaScript. No ORM, SQL imports,
142`migrations.js` bundle, or migration environment variables are needed.
143
144Use either flat files such as `migrations/0001_init.sql` or modern
145drizzle-kit directories such as
146`drizzle/20260919000000_create_users/migration.sql`. Commit the files
147before running Alchemy. Paths are relative to the command's current
148working directory, **not** the source module.
149
150Load the directory in the outer Effect:
151
152```typescript
153import * as Cloudflare from "alchemy/Cloudflare";
154import * as Effect from "effect/Effect";
155
156export class Users extends Cloudflare.DurableObject<Users>()(
157 "Users",
158 Effect.gen(function* () {
159 const migrations = yield* Cloudflare.SqlMigrations("./migrations");
160 const state = yield* Cloudflare.DurableObjectState;
161
162 return Effect.gen(function* () {
163 yield* migrations.apply().pipe(Effect.orDie);
164
165 return {
166 listUsers: () =>
167 state.storage.sql.exec("SELECT id, name FROM users").pipe(
168 Effect.flatMap((cursor) => cursor.toArray()),
169 ),
170 };
171 });
172 }),
173) {}
174```
175
176`migrations.apply()` requires `RuntimeContext` and the current Durable Object
177state. Call it in the **inner** Effect before returning the instance's API;
178it cannot run in the outer construction Effect.
179Its error channel includes `MigrationError` and
180`MigrationHistoryConflictError`. Using `.pipe(Effect.orDie)` here
181prevents activation if the schema cannot be initialized.
182
183The object form selects a different history table:
184
185```typescript
186const migrations = yield* Cloudflare.SqlMigrations({
187 dir: "./drizzle",
188 table: "app_migrations", // default: "__alchemy_migrations"
189});
190```
191
192### Per-instance application
193
194Each object applies pending files when it activates, not during deployment of
195the whole namespace. Each file's SQL and history row commit together:
196
197```text
1980001_create_users → commit SQL + history
1990002_add_email → failure rolls back this file and prevents activation
200next activation → skip 0001, retry 0002
201```
202
203### Adopting Drizzle history
204
205```text
206__drizzle_migrations → copy matching history → __alchemy_migrations
207 missing match → MigrationHistoryConflictError
208```
209
210Adoption is one-way: keep the historical SQL files and use only Alchemy's
211migrator afterward; the old table stays frozen. Changing `migrationsTable`
212on a `migrations.js` bundle does not convert its history.
213
214### Upgrade legacy Drizzle files
215
216For the legacy `meta/_journal.json` layout:
217
218```sh
219pnpm exec drizzle-kit up
220git diff -- drizzle
221git add drizzle
222git commit -m "Upgrade Drizzle migration layout"
223```
224
225See the [Drizzle guide](/sql/drizzle/migrations#durable-object-migrations)
226or [runnable example](https://github.com/alchemy-run/alchemy/tree/main/examples/cloudflare-durable-object-sql)
227for the full setup.
228
229## Where next
230
231- [Postgres](/sql/effect-sql/postgres) /
232 [MySQL](/sql/effect-sql/mysql) / [D1](/sql/effect-sql/d1) — query
233 the migrated database.
234- [Drizzle migrations](/sql/drizzle/migrations) — generate the
235 files from a schema module instead of writing them by hand.
236- [Branch from a shared database](/cloudflare/data/branch-from-shared-database)
237 — per-stage databases built on the same contract.