Skip to content

Drizzle ORM with Neon

Drizzle gives you a typed Postgres schema in plain TypeScript; alchemy’s Drizzle.Schema resource turns it into a deploy-driven migration pipeline. On every deploy the schema is diffed against the last snapshot, pending SQL is regenerated, and Neon.Project / Neon.Branch apply it transactionally — no separate drizzle-kit generate step to forget.

Drizzle.Schema ships in its own providers() layer — merge it next to Neon’s in your stack config:

alchemy.run.ts
import * as Drizzle from "alchemy/Drizzle";
import * as Neon from "alchemy/Neon";
import * as Layer from "effect/Layer";
providers: Layer.mergeAll(Drizzle.providers(), Neon.providers()),

It’s a build-time provider — it owns the migrations directory on disk, not anything in a cloud account.

Point Drizzle.Schema at your schema module and the directory where migrations should land:

import * as Drizzle from "alchemy/Drizzle";
const schema = yield* Drizzle.Schema("app-schema", {
schema: "./src/schema.ts",
out: "./migrations",
});

./src/schema.ts is an ordinary Drizzle module — pgTable(...) definitions and exports. On deploy the provider loads it through drizzle-kit’s programmatic API, diffs it against the latest snapshot under out, and — only when the diff would emit SQL — writes a new {timestamp}_migration/ directory containing migration.sql and snapshot.json. Migration files are meant to be checked in, and removing the resource from the stack never deletes them.

Wire the schema’s out output into the Neon resource’s migrationsDir:

import * as Neon from "alchemy/Neon";
const project = yield* Neon.Project("app-db", {
region: "aws-us-east-1",
});
const branch = yield* Neon.Branch("app-branch", {
project,
migrationsDir: schema.out,
});

Because Neon.Branch depends on schema.out, the engine orders them for you: Drizzle.Schema regenerates pending migration SQL first, then Neon.Branch scans the directory and applies any new files transactionally, recording each in the neon_migrations tracking table so it runs exactly once. The same prop works on Neon.Project if you migrate the default branch directly — see Migrations for the tracking and hashing details.

Change src/schema.ts — add a column, a table — and redeploy:

Terminal window
bun alchemy deploy

The provider diffs the schema against the last snapshot, writes a new migration directory with just the delta SQL, and the branch applies it. When nothing changed, no migration is written and the Neon resource isn’t touched.

The branch’s outputs carry everything a client needs: connectionUri / pooledConnectionUri plus the parsed origin / pooledOrigin shapes (see Connections). On Cloudflare, front the direct origin with Hyperdrive and keep the pooled one for local dev:

const hyperdrive = yield* Cloudflare.Hyperdrive.Connection("app-hyperdrive", {
origin: branch.origin, // direct — Hyperdrive does the pooling
dev: branch.pooledOrigin, // local dev bypasses Hyperdrive
});

Inside the Worker, Drizzle.postgres(conn.connectionString) returns a typed EffectPgDatabase whose queries you yield* directly — the Add Drizzle ORM guide walks through the whole Worker side: binding the connection, relations for typed db.query, and the nodejs_compat flag.

Guides:

Related:

  • Migrations as resources — the canonical explanation of Drizzle.Schema’s generation, diffing, and never-delete semantics.
  • Migrations — how Neon orders, hashes, and tracks the generated files.
  • Connections — direct vs pooled origins.

Reference: