Cloudflare D1
Provision D1 with Alchemy v2 and connect effect-auth through Effect-QB or Drizzle.
Cloudflare D1 is the storage/database in this setup. It is a managed, SQLite-compatible database exposed to a Worker through a D1Database binding. Effect-QB and Drizzle are alternative adapter paths that connect effect-auth's storage services to that same database; they are not databases and do not provision D1.
Choose one adapter. Effect-QB is the direct path and has fewer dependencies. Choose Drizzle when the application already uses the Effect-native Drizzle D1 integration. Both paths use the same effect-auth schema and migrations.
Auth program
|
v
Effect storage services
|
+---- Effect-QB adapter ----+
| |
+---- Drizzle adapter ------+--> Worker DB binding --> Cloudflare D1For broader choices, see Storage Overview, SQLite, or Custom Database. For a complete Worker application, start with the Quick Start.
Install
Install the shared runtime, infrastructure, and Worker types, then add exactly one adapter's dependencies.
bun add @effect-auth/core effect alchemy@2.0.0-beta.61
bun add -d @cloudflare/workers-typesbun add effect-qb@4.0.0-beta.92bun add drizzle-orm@1.0.0-rc.4 @effect/sql-d1@4.0.0-beta.93Keep Effect ecosystem peer versions compatible with the installed effect-auth release. Alchemy v2 is pre-release here, so pin it and review upgrades.
Export the migrations
effect-auth publishes the ordered SQLite migration stream through a public module. Export it to files that Alchemy can apply and commit those files with the application:
import { mkdir } from "node:fs/promises";
import { authStorageMigrations } from "@effect-auth/core/StorageMigrations";
await mkdir("migrations", { recursive: true });
for (const migration of authStorageMigrations) {
await Bun.write(`migrations/${migration.id}.sql`, migration.sql);
}bun scripts/write-auth-migrations.tsRun this again when upgrading effect-auth and review the SQL diff. Migrations are shared by both adapters: Drizzle users do not recreate or push the auth schema with Drizzle Kit.
Provision D1
The D1 resource, binding, and migration lifecycle are common to both adapter paths:
import * as Alchemy from "alchemy";
import * as Cloudflare from "alchemy/Cloudflare";
import * as Effect from "effect/Effect";
export const Database = Cloudflare.D1.Database("AuthDatabase", {
migrationsDir: "./migrations",
});
export const Worker = Cloudflare.Worker("AuthWorker", {
main: "./src/worker.ts",
env: { DB: Database },
});
export default Alchemy.Stack(
"EffectAuth",
{ providers: Cloudflare.providers(), state: Cloudflare.state() },
Effect.gen(function* () {
const database = yield* Database;
const worker = yield* Worker;
return {
databaseId: database.databaseId,
workerUrl: worker.url.as<string>(),
};
})
);Cloudflare.D1.Database creates the database. Passing that resource as Worker.env.DB creates the binding whose runtime value is D1Database. Alchemy applies new .sql files from migrationsDir; append new migrations rather than editing files already applied to an environment.
Provide the storage layer
Create the layer from the Worker's binding and provide it to the auth composition.
import { D1EffectQbSqliteAuthStorageLive } from "@effect-auth/core/EffectQbSqliteStorage";
export interface Env {
readonly DB: D1Database;
}
export const makeAuthStorageLive = (env: Env) =>
D1EffectQbSqliteAuthStorageLive(env.DB);This adapter executes Effect-QB queries directly against the D1 binding.
import { DrizzleD1SqliteAuthStorageLive } from "@effect-auth/core/DrizzleD1SqliteStorage";
export interface Env {
readonly DB: D1Database;
}
export const makeAuthStorageLive = (env: Env) =>
DrizzleD1SqliteAuthStorageLive(env.DB);This constructor creates the Effect-native Drizzle D1 integration. Do not substitute the promise-based drizzle-orm/d1 driver.
Lifecycle
Minimal scripts are sufficient:
{
"type": "module",
"scripts": {
"dev": "alchemy dev",
"deploy": "alchemy deploy",
"destroy": "alchemy destroy"
}
}Make CLOUDFLARE_API_TOKEN available, run bun run dev for a development stack, and run bun run deploy for the selected production stage. Use separate D1 databases per environment. bun run destroy deletes managed resources and must be protected for production.
D1 caveats
- Apply migrations during deployment, never on each request. Back up production data before schema changes.
- D1 binding calls are not a connection-scoped transaction. Do not wrap separate calls in
BEGIN,COMMIT, orSAVEPOINT; the adapters rely on constraints and conditional statements for atomic state changes. - D1 has platform limits, serialized writes, and remote-service latency. Keep queries indexed, avoid unbounded database concurrency, and test against D1 rather than assuming local SQLite behavior.
- The normal
D1Databaseadapter path does not expose D1 Sessions or replica bookmarks. Review Cloudflare's D1 limits and Workers Binding API before enabling replication or setting throughput expectations.