Skip to content

Cloudflare D1

This example creates a D1 database and a Worker serving Better Auth at /api/auth. It includes the auth configuration, database layer, and stack; no tutorial files are required.

Terminal window
bun add @alchemy.run/better-auth better-auth@^1.7.5 kysely

kysely is the SQL adapter peer; D1 needs no separate database driver. Configure Cloudflare credentials through an Alchemy profile before deployment.

src/worker.ts
import { BetterAuth } from "@alchemy.run/better-auth";
import { CloudflareD1 } from "@alchemy.run/better-auth/CloudflareD1";
import * as Cloudflare from "alchemy/Cloudflare";
import * as Effect from "effect/Effect";
const AuthDb = Cloudflare.D1.Database("AuthDb");
export default class AuthApi extends Cloudflare.Worker<AuthApi>()(
"AuthApi",
{
main: import.meta.url,
compatibility: { flags: ["nodejs_compat"] },
},
Effect.gen(function* () {
const auth = yield* BetterAuth({
basePath: "/api/auth",
emailAndPassword: { enabled: true },
});
return { fetch: auth.fetch };
}).pipe(Effect.provide(CloudflareD1(AuthDb))),
) {}

CloudflareD1 registers the native Worker binding and supplies Better Auth’s database service. This auth-only Worker forwards every request to Better Auth; endpoints live under /api/auth.

alchemy.run.ts
import * as Alchemy from "alchemy";
import * as Cloudflare from "alchemy/Cloudflare";
import * as Effect from "effect/Effect";
import AuthApi from "./src/worker.ts";
export default Alchemy.Stack(
"BetterAuthD1",
{
providers: Cloudflare.providers(),
state: Alchemy.localState(),
},
Effect.gen(function* () {
const api = yield* AuthApi;
return { url: api.url.as<string>() };
}),
);

Omitting secret provisions and binds a stable signing secret automatically. Preserve and protect the local .alchemy state directory across deployments.

Terminal window
bunx alchemy deploy

Deployment creates D1 and applies the auth schema through its HTTP API before updating the Worker. Requests use the native binding and never run migrations.

Terminal window
bunx alchemy dev

Development uses the local D1 simulator, including migrations; use the URL printed by Alchemy. The Worker still runs in workerd, so replacing this layer with Bun’s SQLite is not supported.

Set AUTH_ORIGIN to the URL printed by Alchemy, then request the session endpoint:

Terminal window
curl -i "$AUTH_ORIGIN/api/auth/get-session"

D1 has no interactive transactions; plugins requiring them, including SCIM in Better Auth 1.7, need a different database. Automatic migrations are additive, not a data-transfer or destructive schema-migration tool.

For existing applications, preserve the signing secret and review the 1.6 to 1.7 upgrade guide. See configuration for production origins, migrations for schema ownership, and the CloudflareD1 reference for layer options.