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 migrationsDir 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", {
migrationsDir: "./migrations",
});

The tracking table is wrangler-compatible, and you can point at a custom one (e.g. for Drizzle-generated migrations):

const db = yield* Cloudflare.D1.Database("my-db", {
migrationsDir: "./migrations",
migrationsTable: "drizzle_migrations",
});

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: