Migrations
Migrations without an ORM: commit .sql files to a directory and
wire it into the database resource with migrations. Each deploy
applies the pending files — there is nothing else to run.
Write the migrations
Section titled “Write the migrations”-- migrations/0001_init.sqlCREATE TABLE users ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, email TEXT NOT NULL UNIQUE);-- migrations/0002_posts.sqlCREATE TABLE posts ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id), title TEXT NOT NULL);Wire the directory in
Section titled “Wire the directory in”const db = yield* Cloudflare.D1.Database("app-db", { migrations: "./migrations",});The same prop exists on Neon.Project, Neon.Branch,
Fly.Postgres, and the PlanetScale databases and branches.
How files are applied
Section titled “How files are applied”- The directory is scanned recursively for
.sqlfiles. - Files are ordered by numeric prefix (
0001_init.sql,0002_posts.sql), then by name. - Each file is content-hashed and applied files are recorded, so a deploy only runs what’s new — an unchanged directory is a noop.
- Application is ordered and stops on the first failure; the failed file is retried on the next deploy.
The tracking table
Section titled “The tracking table”Applied migrations are recorded in one Alchemy-owned table,
__alchemy_migrations, on every database:
CREATE TABLE IF NOT EXISTS "__alchemy_migrations" ( id SERIAL PRIMARY KEY, -- INTEGER on SQLite hash text NOT NULL, -- sha256 of the file created_at bigint, -- millis from a timestamp prefix, if any name text, -- the applied-detection key applied_at timestamp with time zone DEFAULT now());Detection is by name, so renaming an applied file is a schema
change, not a cosmetic one.
To put the bookkeeping in a differently-named table, use the object form of the prop:
const db = yield* Cloudflare.D1.Database("app-db", { migrations: { dir: "./migrations", table: "my_migrations", // default: "__alchemy_migrations" },});Adopting an existing database
Section titled “Adopting an existing database”A database previously migrated with drizzle-kit, Prisma, or
wrangler needs no baselining. On the first deploy, Alchemy finds
the old tool’s tracking table — __drizzle_migrations (in the
drizzle schema on Postgres), _prisma_migrations, or
d1_migrations — copies its applied history into
__alchemy_migrations once, and leaves the old table frozen:
never written, never dropped. Only migrations the old tool hadn’t
applied yet actually run.
This is a one-way move. After adoption, Alchemy’s table is the only bookkeeping — the old tool’s table stops reflecting reality, so stop running its migrate command.
Two guard rails:
- Every recorded row must match a local migration file; an orphaned
row fails the deploy with
MigrationHistoryConflictError— it means migrations were applied that your checkout doesn’t have. - A failed Prisma migration (
finished_atNULL) blocks adoption until repaired withprisma migrate resolve; rolled-back rows are skipped.
Databases migrated by older versions of Alchemy are upgraded to the current table shape in place, at whatever table name their state recorded — no action needed.
Each database documents its own mechanics — transactionality and quirks:
| Database | Mechanics |
|---|---|
| Cloudflare D1 | D1 migrations — batched application (no transactions over HTTP) |
| Neon | Neon migrations — applied transactionally on the branch |
| PlanetScale | PlanetScale migrations — the same contract on Postgres and MySQL branches |
| Fly Managed Postgres | Fly Postgres — applied over the direct URI |
Generated migrations target the same contract
Section titled “Generated migrations target the same contract”Drizzle.Schema emits into the same
shape: its out output is a migrations directory, so
migrations: schema orders generation before application in one
deploy. Any tool that writes ordered .sql files into a directory
already works — drizzle-kit’s <timestamp>_<name>/migration.sql
layout is recognized and keyed by directory name.
Where next
Section titled “Where next”- Postgres / MySQL / D1 — query the migrated database.
- Drizzle migrations — generate the files from a schema module instead of writing them by hand.
- Branch from a shared database — per-stage databases built on the same contract.