D1
D1 is Cloudflare’s serverless SQLite database. There’s nothing to provision or scale — you get a SQL database addressed by name, with reads served close to your users when read replication is enabled.
Reach for D1 when your app needs relational data without operating a database: user tables, content, app state queried with plain SQL. If you need Postgres or MySQL (or already have one), front it with Hyperdrive instead.
Create a database
Section titled “Create a database”A database is one resource declaration:
import * as Cloudflare from "alchemy/Cloudflare";
export const Database = Cloudflare.D1.Database("Database");Yield it inside your Stack like any other resource:
import * as Alchemy from "alchemy";import * as Cloudflare from "alchemy/Cloudflare";import * as Effect from "effect/Effect";import { Database } from "./src/database.ts";
export default Alchemy.Stack( "MyApp", { providers: Cloudflare.providers(), state: Cloudflare.state(), }, Effect.gen(function* () { const db = yield* Database;
return { databaseName: db.databaseName }; }),);Migrations
Section titled “Migrations”Point migrations at a folder of .sql files. Files are sorted
by numeric prefix (0001_, 0002_, …) and applied in order as part
of every deploy; already-applied migrations are skipped:
const db = yield* Cloudflare.D1.Database("my-db", { migrations: "./migrations",});Bookkeeping lives in Alchemy’s __alchemy_migrations table. A
database previously migrated with wrangler d1 migrations apply or
drizzle-kit migrate is
adopted automatically:
the old tool’s applied history is copied in once and its table is
frozen — never written, never dropped. D1 has no transactions over
HTTP, so each migration and its bookkeeping row are sent as one
batched query, matching wrangler’s own behavior.
The migration files themselves don’t have to be hand-written — Drizzle.Schema can generate them from a Drizzle schema on deploy. Drizzle on D1 walks through the whole flow: schema → generated migrations → typed queries from the Worker.
Query from a Worker
Section titled “Query from a Worker”Bind the database into a Worker with Cloudflare.D1.QueryDatabase
and provide the QueryDatabaseBinding layer. The client exposes
prepare, exec, batch, and raw; prepared statements execute
with all, first, or run:
import * as Cloudflare from "alchemy/Cloudflare";import * as Effect from "effect/Effect";import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse";import { Database } from "./database.ts";
export default Cloudflare.Worker( "Worker", { main: import.meta.url }, Effect.gen(function* () { // init: register the binding, get a typed client const db = yield* Cloudflare.D1.QueryDatabase(Database);
return { fetch: Effect.gen(function* () { // runtime: run queries const results = yield* db .prepare("SELECT * FROM users WHERE id = ?") .bind(1) .all(); return yield* HttpServerResponse.json(results); }), }; }).pipe(Effect.provide(Cloudflare.D1.QueryDatabaseBinding)),);prepare and bind are synchronous plan builders — only the
executors (all, first, run, raw) round-trip to the database,
and those return Effects. For libraries that want the native
D1Database object (e.g. Better Auth), use the client’s raw
escape hatch.
SQL with @effect/sql-d1
Section titled “SQL with @effect/sql-d1”Prefer tagged-template SQL over hand-written prepared statements?
SQL.D1 (from alchemy/SQL/D1 — each low-level effect-sql client
lives on its own subpath) wraps the same binding in an
@effect/sql-d1 D1Client —
interpolated values are parameterized, every query is an Effect, and
failures surface as typed SqlErrors:
import * as SQL from "alchemy/SQL/D1";
Effect.gen(function* () { const d1 = yield* Cloudflare.D1.QueryDatabase(Database); const sql = yield* SQL.D1(d1);
return { fetch: Effect.gen(function* () { const users = yield* sql`SELECT * FROM users WHERE id = ${id}`; return yield* HttpServerResponse.json({ users }); }), };}).pipe(Effect.provide(Cloudflare.D1.QueryDatabaseBinding));The client is built lazily on the first query and memoized per
execution — a fetch/queue/scheduled event, a Durable Object
call, or a Workflow run — then torn down when the event settles.
D1Client implements Effect’s generic SqlClient interface, so
services written against SqlClient.SqlClient run on D1 unchanged.
Provide both tags with the layer form:
import * as SqlClient from "effect/unstable/sql/SqlClient";
const makeApp = Effect.gen(function* () { const sql = yield* SqlClient.SqlClient; // ... services that only know about SqlClient});
const app = yield* makeApp.pipe( Effect.provide(SQL.D1Layer(d1)),);The complete runnable project is
cloudflare-effect-sql-d1.
The full client API, the Layer pattern, and the lifecycle contract
live in the SQL hub — Effect SQL: D1 and
Connection lifecycle. For typed schemas
and an ORM on top of the same binding, see
Drizzle on D1.
Where next
Section titled “Where next”Guides:
- Drizzle on D1 — typed schemas,
generated migrations, and the
effect-d1driver on this database. cloudflare-effect-sql-d1— the runnable@effect/sql-d1example.
Related:
- Workers — where your queries run.
- Hyperdrive — for external Postgres/MySQL instead of SQLite.
Reference: