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.

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.

Guides:

  • Add Drizzle ORM — typed schemas and generated migrations (the D1 resource’s migrationsTable prop accepts Drizzle’s).

Related:

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

Reference: