1---
2title: Add Drizzle ORM
3description: Deploy a Cloudflare Worker with Drizzle queries, Hyperdrive connections, and reviewed, committed migrations on Neon or PlanetScale.
4---
5
6import { Code, Tabs, TabItem } from "@astrojs/starlight/components";
7
8export const drizzleDeps = `drizzle-orm@1.0.0-rc.5-ab785fc @effect/sql-pg pg`;
9export const drizzleDevDeps = `drizzle-kit@1.0.0-rc.5-ab785fc @types/pg`;
10
11This walkthrough connects a Worker to Postgres through Hyperdrive; [SQL](/sql) owns the portable client APIs. For Cloudflare's SQLite database, use [Drizzle on D1](/cloudflare/data/d1-drizzle).
12
13## Install the client and generator
14
15<Tabs syncKey="pkgManager">
16 <TabItem label="bun" icon="bun">
17 <Code code={`bun add ${drizzleDeps}\nbun add -d ${drizzleDevDeps}`} lang="sh" />
18 </TabItem>
19 <TabItem label="npm" icon="npm">
20 <Code code={`npm install ${drizzleDeps}\nnpm install -D ${drizzleDevDeps}`} lang="sh" />
21 </TabItem>
22 <TabItem label="pnpm" icon="pnpm">
23 <Code code={`pnpm add ${drizzleDeps}\npnpm add -D ${drizzleDevDeps}`} lang="sh" />
24 </TabItem>
25 <TabItem label="yarn" icon="seti:yarn">
26 <Code code={`yarn add ${drizzleDeps}\nyarn add -D ${drizzleDevDeps}`} lang="sh" />
27 </TabItem>
28</Tabs>
29
30Keep the exact Drizzle prerelease pins together for the Effect integration; `drizzle-kit` is a development dependency.
31
32## Define the schema
33
34```typescript
35// src/schema.ts
36import { integer, pgTable, serial, text, timestamp } from "drizzle-orm/pg-core";
37
38export const Users = pgTable("users", {
39 id: serial("id").primaryKey(),
40 email: text("email").notNull().unique(),
41 name: text("name").notNull(),
42 createdAt: timestamp("created_at", { withTimezone: true })
43 .notNull()
44 .defaultNow(),
45});
46
47export const Posts = pgTable("posts", {
48 id: serial("id").primaryKey(),
49 userId: integer("user_id")
50 .notNull()
51 .references(() => Users.id, { onDelete: "cascade" }),
52 title: text("title").notNull(),
53 body: text("body").notNull(),
54 createdAt: timestamp("created_at", { withTimezone: true })
55 .notNull()
56 .defaultNow(),
57});
58```
59
60This is an ordinary Postgres schema; it has no Worker-specific configuration.
61
62## Configure migration generation
63
64```typescript
65// drizzle.config.ts
66import { defineConfig } from "drizzle-kit";
67
68export default defineConfig({
69 schema: "./src/schema.ts",
70 out: "./migrations",
71 dialect: "postgresql",
72});
73```
74
75The generator writes SQL and snapshots to `migrations`; it does not need database credentials.
76
77## Generate and review migrations
78
79```sh
80bunx drizzle-kit generate
81git diff -- src/schema.ts migrations
82git status --short
83git add src/schema.ts drizzle.config.ts migrations
84git diff --cached
85git commit -m "Add database migration"
86```
87
88Generate whenever the schema changes, review the schema, SQL, and snapshots together, and commit them before deployment. The optional [`Drizzle.Schema` resource](/providers/drizzle/schema) is a separate integration, not part of this workflow.
89
90## Configure the database
91
92Use [Neon's database configuration](/neon/guides/drizzle#feed-migrations-from-the-schema) to export `NeonDb` from `src/Db.ts` with `migrations: "./migrations"`. That guide owns the project, branch, and connection options; [PlanetScale configuration](/planetscale/guides/drizzle) is the alternative below.
93
94## Add Hyperdrive
95
96```typescript
97// src/Hyperdrive.ts
98import * as Cloudflare from "alchemy/Cloudflare";
99import * as Effect from "effect/Effect";
100import { NeonDb } from "./Db.ts";
101
102export const Hyperdrive = Effect.gen(function* () {
103 const { branch } = yield* NeonDb;
104 return yield* Cloudflare.Hyperdrive.Connection("app-hyperdrive", {
105 origin: branch.origin,
106 dev: branch.pooledOrigin,
107 });
108});
109```
110
111Hyperdrive pools the direct Neon origin; local development uses the pooled origin when it bypasses Hyperdrive.
112
113## Open the connection with `Drizzle.Postgres`
114
115```typescript
116// src/Api.ts
117import * as Cloudflare from "alchemy/Cloudflare";
118import * as Drizzle from "alchemy/Drizzle/Postgres";
119import * as Effect from "effect/Effect";
120import * as HttpServerResponse from "effect/http/HttpServerResponse";
121import { Hyperdrive } from "./Hyperdrive.ts";
122import { Users } from "./schema.ts";
123
124export default class Api extends Cloudflare.Worker<Api>()(
125 "Api",
126 {
127 main: import.meta.url,
128 compatibility: { flags: ["nodejs_compat"] },
129 },
130 Effect.gen(function* () {
131 const hd = yield* Cloudflare.Hyperdrive.Connect(Hyperdrive);
132 const db = yield* Drizzle.Postgres(hd.connectionString);
133
134 return {
135 fetch: Effect.gen(function* () {
136 const users = yield* db.select().from(Users);
137 return yield* HttpServerResponse.json(users);
138 }),
139 };
140 }).pipe(Effect.provide(Cloudflare.Hyperdrive.ConnectBinding)),
141) {}
142```
143
144`nodejs_compat` enables the Node APIs used by `pg`, and `ConnectBinding` supplies the Worker's Hyperdrive binding. Queries yield directly as Effects and acquire their pool only within the request's [scope](/sql/effect-sql/lifecycle).
145
146## Register the providers
147
148```typescript
149// alchemy.run.ts
150import * as Alchemy from "alchemy";
151import * as Cloudflare from "alchemy/Cloudflare";
152import * as Neon from "alchemy/Neon";
153import * as Effect from "effect/Effect";
154import * as Layer from "effect/Layer";
155import Api from "./src/Api.ts";
156
157export default Alchemy.Stack(
158 "MyStack",
159 {
160 providers: Layer.mergeAll(Cloudflare.providers(), Neon.providers()),
161 state: Alchemy.localState(),
162 },
163 Effect.gen(function* () {
164 const api = yield* Api;
165 return { url: api.url };
166 }),
167);
168```
169
170Only the runtime and database providers are needed; committed migration files do not require `Drizzle.providers()`.
171
172## Deploy
173
174```sh
175bun alchemy deploy
176```
177
178The configured branch applies pending committed SQL before the Worker serves queries; deployment does not generate new migrations. Keep an existing `drizzle-kit migrate` workflow unless you deliberately choose Alchemy-managed application, as described in [migration ownership](/sql/drizzle/migrations).
179
180## Define relations for typed `db.query`
181
182```diff lang="typescript"
183// src/schema.ts
184+import { defineRelations } from "drizzle-orm";
185
186+export const relations = defineRelations({ Users, Posts }, (t) => ({
187+ Users: { posts: t.many.Posts() },
188+ Posts: {
189+ user: t.one.Users({
190+ from: t.Posts.userId,
191+ to: t.Users.id,
192+ }),
193+ },
194+}));
195```
196
197Append the relations after the table definitions; this metadata enables the relational query API without changing migration SQL.
198
199## Pass `relations` to `Drizzle.Postgres`
200
201```diff lang="typescript"
202// src/Api.ts
203-import { Users } from "./schema.ts";
204+import { relations, Users } from "./schema.ts";
205
206-const db = yield* Drizzle.Postgres(hd.connectionString);
207+const db = yield* Drizzle.Postgres(hd.connectionString, { relations });
208```
209
210The returned client now exposes `db.query.Users` and `db.query.Posts` with typed relation names.
211
212## Query related rows
213
214```diff lang="typescript"
215// src/Api.ts
216-const users = yield* db.select().from(Users);
217-return yield* HttpServerResponse.json(users);
218+const user = yield* db.query.Users.findFirst({
219+ where: { id: 1 },
220+ with: { posts: true },
221+});
222+return yield* HttpServerResponse.json({ user });
223```
224
225`with: { posts: true }` includes the related posts in the inferred result type.
226
227## Iterate on the schema
228
229```sh
230bunx drizzle-kit generate
231git add src/schema.ts migrations
232git diff --cached
233git commit -m "Update database schema"
234bun alchemy deploy
235```
236
237Review each generated change before committing and deploy only those committed files. Reverting a TypeScript schema does not undo applied SQL; author and review a new migration or follow your database's restore procedure.
238
239## Use PlanetScale instead
240
241```diff lang="typescript"
242// alchemy.run.ts
243-import * as Neon from "alchemy/Neon";
244+import * as Planetscale from "alchemy/Planetscale";
245
246- providers: Layer.mergeAll(Cloudflare.providers(), Neon.providers()),
247+ providers: Layer.mergeAll(Cloudflare.providers(), Planetscale.providers()),
248```
249
250Use the [PlanetScale guide](/planetscale/guides/drizzle) to export `PlanetscaleDb` with committed migrations; it owns both Postgres role and MySQL password configuration.
251
252## Connect a PlanetScale Postgres role
253
254```typescript
255// src/Hyperdrive.ts
256import * as Cloudflare from "alchemy/Cloudflare";
257import * as Effect from "effect/Effect";
258import { PlanetscaleDb } from "./Db.ts";
259
260export const Hyperdrive = Effect.gen(function* () {
261 const { role } = yield* PlanetscaleDb;
262 return yield* Cloudflare.Hyperdrive.Connection("app-hyperdrive", {
263 origin: role.origin,
264 dev: role.pooledOrigin,
265 caching: { disabled: true },
266 });
267});
268```
269
270The Worker, Postgres schema, and queries remain unchanged.
271
272## Connect a PlanetScale MySQL password
273
274```typescript
275// Inside the Hyperdrive effect, replacing the Postgres role configuration:
276const { password } = yield* PlanetscaleDb;
277return yield* Cloudflare.Hyperdrive.Connection("app-hyperdrive", {
278 origin: password.origin,
279 caching: { disabled: true },
280});
281```
282
283Select the MySQL resource variant in the [PlanetScale guide](/planetscale/guides/drizzle#mysql-apply-and-connect); there is no pooled password origin.
284
285## Select the MySQL client
286
287```sh
288bun add @effect/sql-mysql2 mysql2
289```
290
291```diff lang="typescript"
292// src/Api.ts
293-import * as Drizzle from "alchemy/Drizzle/Postgres";
294+import * as Drizzle from "alchemy/Drizzle/MySQL";
295
296-const db = yield* Drizzle.Postgres(hd.connectionString, { relations });
297+const db = yield* Drizzle.MySQL(hd.connectionString, { relations });
298```
299
300Use the [MySQL schema and generator configuration](/sql/drizzle/mysql), then generate, review, and commit MySQL migrations before deploying. The client uses [Workers-safe defaults](/sql/effect-sql/mysql#workers-defaults); MySQL-specific query differences stay in the portable client guide.
301
302## Where to from here
303
304Use [Neon](/neon) or [PlanetScale](/planetscale) for database service behavior and [SQL databases](/sql/databases) to discover other providers. [Branch from a shared database](/cloudflare/data/branch-from-shared-database) covers the Worker deployment pattern for preview environments.