1import type { RuntimeContext } from "alchemy";
2import * as Cloudflare from "alchemy/Cloudflare";
3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";
5import type * as Scope from "effect/Scope";
6import {
7 Database,
8 type DatabaseService,
9 type DirectDatabase,
10} from "./Database.ts";
11import type { BetterAuthMigrationError } from "./Errors.ts";
12
13/**
14 * Cloudflare D1 database layer for Better Auth.
15 *
16 * At runtime the Worker reaches the database through its native D1 binding
17 * (the layer bakes in `QueryDatabaseBinding`). At deploy time, schema
18 * migrations run over the D1 HTTP query API — which also transparently
19 * targets the local D1 simulator under `alchemy dev`.
20 *
21 *
22 * ### Using D1 as the auth database
23 * Provide the layer on the Worker impl effect that yields `BetterAuth`.
24 * The database resource can be referenced from module scope — the layer
25 * accepts the resource or its Effect.
26 * **Example:** Worker with a D1-backed BetterAuth
27 * ```typescript
28 * import { BetterAuth } from "@alchemy.run/better-auth";
29 * import { CloudflareD1 } from "@alchemy.run/better-auth/CloudflareD1";
30 * import * as Cloudflare from "alchemy/Cloudflare";
31 *
32 * export const AuthDb = Cloudflare.D1.Database("AuthDb");
33 *
34 * export default class Api extends Cloudflare.Worker<Api>()(
35 * "Api",
36 * { main: import.meta.url, compatibility: { flags: ["nodejs_compat"] } },
37 * Effect.gen(function* () {
38 * const auth = yield* BetterAuth({
39 * basePath: "/auth",
40 * emailAndPassword: { enabled: true },
41 * });
42 * return {
43 * fetch: Effect.gen(function* () {
44 * const request = yield* HttpServerRequest;
45 * if (request.url.startsWith("/auth")) {
46 * return yield* auth.fetch;
47 * }
48 * const session = yield* auth.getSession();
49 * return yield* HttpServerResponse.json({ user: session?.user ?? null });
50 * }),
51 * };
52 * }).pipe(Effect.provide(CloudflareD1(AuthDb))),
53 * ) {}
54 * ```
55 *
56 * ### Migrations
57 * The schema migration Action connects over the D1 HTTP API at deploy
58 * time — the Worker's native binding is never used outside the deployed
59 * runtime, and no migration code ships in the Worker bundle.
60 * **Example:** Opting out of automatic migrations
61 * ```typescript
62 * const auth = yield* BetterAuth({
63 * migrate: false, // manage the schema yourself
64 * emailAndPassword: { enabled: true },
65 * });
66 * ```
67 *
68 * @layer
69 * @provides BetterAuth.Database
70 * @product D1
71 */
72export const CloudflareD1 = (
73 database:
74 | Cloudflare.D1.Database
75 | Effect.Effect<Cloudflare.D1.Database, never, any>,
76) =>
77 Layer.effect(
78 Database,
79 Effect.gen(function* () {
80 const db = Effect.isEffect(database)
81 ? yield* database as Effect.Effect<Cloudflare.D1.Database>
82 : database;
83 const client = yield* Cloudflare.D1.QueryDatabase(db);
84 const service: DatabaseService = {
85 provider: "sqlite",
86 runtime: client.raw,
87 };
88 // Migration support references the deploy-time HTTP/local D1 client
89 // (`QueryDatabaseLocal`), which must never reach a Worker bundle —
90 // this branch is dead-code-eliminated from deployed runtimes, which
91 // also tree-shakes the local-runtime machinery out of the bundle.
92 if (!globalThis.__ALCHEMY_RUNTIME__) {
93 return {
94 ...service,
95 migrate: {
96 identity: { databaseId: db.databaseId } as Record<string, unknown>,
97 connect: Effect.gen(function* () {
98 // Init half — runs under the migration Action's capture
99 // context: `QueryDatabase(database)` yields the databaseId
100 // Output, recording the dependency; the client's `raw` is a
101 // deferred accessor that resolves at apply.
102 const local = yield* Cloudflare.D1.QueryDatabase(db);
103 // Apply half — materialize the HTTP D1 facade.
104 return Effect.map(
105 local.raw,
106 (database) => database as DirectDatabase,
107 );
108 }).pipe(
109 Effect.provide(Cloudflare.D1.QueryDatabaseLocal),
110 (effect) =>
111 // The HTTP client's build requirements (Cloudflare
112 // credentials, HttpClient) are ambient during stack-eval
113 // where the init half runs.
114 effect as unknown as Effect.Effect<
115 Effect.Effect<
116 DirectDatabase,
117 BetterAuthMigrationError,
118 Scope.Scope
119 >,
120 never,
121 RuntimeContext
122 >,
123 ),
124 },
125 } satisfies DatabaseService;
126 }
127 return service;
128 }),
129 ).pipe(Layer.provide(Cloudflare.D1.QueryDatabaseBinding));