Add Drizzle ORM
This walkthrough connects a Worker to Postgres through Hyperdrive; SQL owns the portable client APIs. For Cloudflare’s SQLite database, use Drizzle on D1.
Install the client and generator
Section titled “Install the client and generator”bun add drizzle-orm@1.0.0-rc.5-ab785fc @effect/sql-pg pgbun add -d drizzle-kit@1.0.0-rc.5-ab785fc @types/pgnpm install drizzle-orm@1.0.0-rc.5-ab785fc @effect/sql-pg pgnpm install -D drizzle-kit@1.0.0-rc.5-ab785fc @types/pgpnpm add drizzle-orm@1.0.0-rc.5-ab785fc @effect/sql-pg pgpnpm add -D drizzle-kit@1.0.0-rc.5-ab785fc @types/pgyarn add drizzle-orm@1.0.0-rc.5-ab785fc @effect/sql-pg pgyarn add -D drizzle-kit@1.0.0-rc.5-ab785fc @types/pgKeep the exact Drizzle prerelease pins together for the Effect integration; drizzle-kit is a development dependency.
Define the schema
Section titled “Define the schema”import { integer, pgTable, serial, text, timestamp } from "drizzle-orm/pg-core";
export const Users = pgTable("users", { id: serial("id").primaryKey(), email: text("email").notNull().unique(), name: text("name").notNull(), createdAt: timestamp("created_at", { withTimezone: true }) .notNull() .defaultNow(),});
export const Posts = pgTable("posts", { id: serial("id").primaryKey(), userId: integer("user_id") .notNull() .references(() => Users.id, { onDelete: "cascade" }), title: text("title").notNull(), body: text("body").notNull(), createdAt: timestamp("created_at", { withTimezone: true }) .notNull() .defaultNow(),});This is an ordinary Postgres schema; it has no Worker-specific configuration.
Configure migration generation
Section titled “Configure migration generation”import { defineConfig } from "drizzle-kit";
export default defineConfig({ schema: "./src/schema.ts", out: "./migrations", dialect: "postgresql",});The generator writes SQL and snapshots to migrations; it does not need database credentials.
Generate and review migrations
Section titled “Generate and review migrations”bunx drizzle-kit generategit diff -- src/schema.ts migrationsgit status --shortgit add src/schema.ts drizzle.config.ts migrationsgit diff --cachedgit commit -m "Add database migration"Generate whenever the schema changes, review the schema, SQL, and snapshots together, and commit them before deployment. The optional Drizzle.Schema resource is a separate integration, not part of this workflow.
Configure the database
Section titled “Configure the database”Use Neon’s database configuration to export NeonDb from src/Db.ts with migrations: "./migrations". That guide owns the project, branch, and connection options; PlanetScale configuration is the alternative below.
Add Hyperdrive
Section titled “Add Hyperdrive”import * as Cloudflare from "alchemy/Cloudflare";import * as Effect from "effect/Effect";import { NeonDb } from "./Db.ts";
export const Hyperdrive = Effect.gen(function* () { const { branch } = yield* NeonDb; return yield* Cloudflare.Hyperdrive.Connection("app-hyperdrive", { origin: branch.origin, dev: branch.pooledOrigin, });});Hyperdrive pools the direct Neon origin; local development uses the pooled origin when it bypasses Hyperdrive.
Open the connection with Drizzle.Postgres
Section titled “Open the connection with Drizzle.Postgres”import * as Cloudflare from "alchemy/Cloudflare";import * as Drizzle from "alchemy/Drizzle/Postgres";import * as Effect from "effect/Effect";import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse";import { Hyperdrive } from "./Hyperdrive.ts";import { Users } from "./schema.ts";
export default class Api extends Cloudflare.Worker<Api>()( "Api", { main: import.meta.url, compatibility: { flags: ["nodejs_compat"] }, }, Effect.gen(function* () { const hd = yield* Cloudflare.Hyperdrive.Connect(Hyperdrive); const db = yield* Drizzle.Postgres(hd.connectionString);
return { fetch: Effect.gen(function* () { const users = yield* db.select().from(Users); return yield* HttpServerResponse.json(users); }), }; }).pipe(Effect.provide(Cloudflare.Hyperdrive.ConnectBinding)),) {}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.
Register the providers
Section titled “Register the providers”import * as Alchemy from "alchemy";import * as Cloudflare from "alchemy/Cloudflare";import * as Neon from "alchemy/Neon";import * as Effect from "effect/Effect";import * as Layer from "effect/Layer";import Api from "./src/Api.ts";
export default Alchemy.Stack( "MyStack", { providers: Layer.mergeAll(Cloudflare.providers(), Neon.providers()), state: Alchemy.localState(), }, Effect.gen(function* () { const api = yield* Api; return { url: api.url }; }),);Only the runtime and database providers are needed; committed migration files do not require Drizzle.providers().
Deploy
Section titled “Deploy”bun alchemy deployThe 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.
Define relations for typed db.query
Section titled “Define relations for typed db.query”import { defineRelations } from "drizzle-orm";
export const relations = defineRelations({ Users, Posts }, (t) => ({ Users: { posts: t.many.Posts() }, Posts: { user: t.one.Users({ from: t.Posts.userId, to: t.Users.id, }), },}));Append the relations after the table definitions; this metadata enables the relational query API without changing migration SQL.
Pass relations to Drizzle.Postgres
Section titled “Pass relations to Drizzle.Postgres”import { Users } from "./schema.ts";import { relations, Users } from "./schema.ts";
const db = yield* Drizzle.Postgres(hd.connectionString);const db = yield* Drizzle.Postgres(hd.connectionString, { relations });The returned client now exposes db.query.Users and db.query.Posts with typed relation names.
Query related rows
Section titled “Query related rows”const users = yield* db.select().from(Users);return yield* HttpServerResponse.json(users);const user = yield* db.query.Users.findFirst({ where: { id: 1 }, with: { posts: true },});return yield* HttpServerResponse.json({ user });with: { posts: true } includes the related posts in the inferred result type.
Iterate on the schema
Section titled “Iterate on the schema”bunx drizzle-kit generategit add src/schema.ts migrationsgit diff --cachedgit commit -m "Update database schema"bun alchemy deployReview 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.
Use PlanetScale instead
Section titled “Use PlanetScale instead”import * as Neon from "alchemy/Neon";import * as Planetscale from "alchemy/Planetscale";
providers: Layer.mergeAll(Cloudflare.providers(), Neon.providers()), providers: Layer.mergeAll(Cloudflare.providers(), Planetscale.providers()),Use the PlanetScale guide to export PlanetscaleDb with committed migrations; it owns both Postgres role and MySQL password configuration.
Connect a PlanetScale Postgres role
Section titled “Connect a PlanetScale Postgres role”import * as Cloudflare from "alchemy/Cloudflare";import * as Effect from "effect/Effect";import { PlanetscaleDb } from "./Db.ts";
export const Hyperdrive = Effect.gen(function* () { const { role } = yield* PlanetscaleDb; return yield* Cloudflare.Hyperdrive.Connection("app-hyperdrive", { origin: role.origin, dev: role.pooledOrigin, caching: { disabled: true }, });});The Worker, Postgres schema, and queries remain unchanged.
Connect a PlanetScale MySQL password
Section titled “Connect a PlanetScale MySQL password”// Inside the Hyperdrive effect, replacing the Postgres role configuration:const { password } = yield* PlanetscaleDb;return yield* Cloudflare.Hyperdrive.Connection("app-hyperdrive", { origin: password.origin, caching: { disabled: true },});Select the MySQL resource variant in the PlanetScale guide; there is no pooled password origin.
Select the MySQL client
Section titled “Select the MySQL client”bun add @effect/sql-mysql2 mysql2import * as Drizzle from "alchemy/Drizzle/Postgres";import * as Drizzle from "alchemy/Drizzle/MySQL";
const db = yield* Drizzle.Postgres(hd.connectionString, { relations });const db = yield* Drizzle.MySQL(hd.connectionString, { relations });Use the MySQL schema and generator configuration, then generate, review, and commit MySQL migrations before deploying. The client uses Workers-safe defaults; MySQL-specific query differences stay in the portable client guide.
Where to from here
Section titled “Where to from here”Use Neon or PlanetScale for database service behavior and SQL databases to discover other providers. Branch from a shared database covers the Worker deployment pattern for preview environments.