Skip to content

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.

A database is one resource declaration:

src/database.ts
import * as Cloudflare from "alchemy/Cloudflare";
export const Database = Cloudflare.D1.Database("Database");

Yield it inside your Stack like any other resource:

alchemy.run.ts
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 };
}),
);

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.

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:

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

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.

Guides:

Related:

  • Workers — where your queries run.
  • Hyperdrive — for external Postgres/MySQL instead of SQLite.

Reference: