Skip to content

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.

Terminal window
pnpm add https://pkg.alchemy.run/@alchemy.run/drizzle/pr:2930:a10573e drizzle-orm@1.0.0-rc.5 @effect/sql-pg
pnpm add -D drizzle-kit@1.0.0-rc.5

Keep the exact Drizzle prerelease pins together for the Effect integration; drizzle-kit is a development dependency.

src/schema.ts
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.

drizzle.config.ts
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.

Terminal window
pnpm drizzle-kit generate
git diff -- src/schema.ts migrations
git status --short
git add src/schema.ts drizzle.config.ts migrations
git diff --cached
git 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.

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.

src/Hyperdrive.ts
import * as Cloudflare from "@alchemy.run/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.

src/Api.ts
import * as Cloudflare from "@alchemy.run/cloudflare";
import * as Drizzle from "@alchemy.run/drizzle/Postgres";
import * as Effect from "effect/Effect";
import * as HttpServerResponse from "effect/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 socket and TLS APIs @effect/sql-pg uses, and ConnectBinding supplies the Worker’s Hyperdrive binding. Queries yield directly as Effects and acquire their pool only within the request’s scope.

alchemy.run.ts
import * as Alchemy from "alchemy";
import * as Cloudflare from "@alchemy.run/cloudflare";
import * as Neon from "@alchemy.run/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().

Terminal window
pnpm alchemy deploy

The 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.

src/schema.ts
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.

src/Api.ts
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.

src/Api.ts
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.

Terminal window
pnpm drizzle-kit generate
git add src/schema.ts migrations
git diff --cached
git commit -m "Update database schema"
pnpm alchemy deploy

Review 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.

alchemy.run.ts
import * as Neon from "@alchemy.run/neon";
import * as Planetscale from "@alchemy.run/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.

src/Hyperdrive.ts
import * as Cloudflare from "@alchemy.run/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.

// 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.

Terminal window
pnpm add @effect/sql-mysql2
src/Api.ts
import * as Drizzle from "@alchemy.run/drizzle/Postgres";
import * as Drizzle from "@alchemy.run/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.

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.