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 Redacted from "effect/Redacted";
6import { Database } from "./Database.ts";
7import { makeMySQLService, type MySQLOptions } from "./MySQL.ts";
8import {
9 makePostgresService,
10 type MigrateSource,
11 type PostgresOptions,
12} from "./Postgres.ts";
13
14export interface CloudflareHyperdriveOptions {
15 /**
16 * Origin dialect behind the Hyperdrive connection.
17 * @default "postgres"
18 */
19 readonly dialect?: "postgres" | "mysql";
20 /**
21 * Deploy-resolvable ORIGIN connection source for schema migrations.
22 * Hyperdrive's own connection string is minted per-invocation inside the
23 * Worker and cannot be used at deploy time — pass the origin database URL
24 * (e.g. Neon `branch.connectionUri`) or leave unset to skip migrations.
25 */
26 readonly migrate?: MigrateSource;
27 readonly pool?: PostgresOptions["pool"] | MySQLOptions["pool"];
28}
29
30/**
31 * Cloudflare Hyperdrive layer for Better Auth — pooled TCP Postgres (or
32 * MySQL) through a Hyperdrive connection.
33 *
34 * Hyperdrive's connection string is minted per-invocation inside the
35 * Worker and cannot be used at deploy time, so pass the ORIGIN database
36 * URL as `migrate` for deploy-time schema migrations (or omit it to skip
37 * them).
38 *
39 *
40 * ### Pooled Postgres through Hyperdrive
41 * The Worker connects over TCP through Hyperdrive's pooler; auth reads
42 * should disable Hyperdrive caching so sessions are never stale.
43 * **Example:** Hyperdrive over a Neon origin
44 * ```typescript
45 * import { BetterAuth } from "@alchemy.run/better-auth";
46 * import { CloudflareHyperdrive } from "@alchemy.run/better-auth/CloudflareHyperdrive";
47 *
48 * export const PgProject = Neon.Project("AuthDb");
49 * export const Hyperdrive = Effect.gen(function* () {
50 * const project = yield* PgProject;
51 * return yield* Cloudflare.Hyperdrive.Connection("AuthHd", {
52 * origin: project.origin,
53 * caching: { disabled: true },
54 * });
55 * });
56 *
57 * Effect.gen(function* () {
58 * const auth = yield* BetterAuth({ emailAndPassword: { enabled: true } });
59 * return { fetch: ... };
60 * }).pipe(
61 * Effect.provide(
62 * Layer.unwrap(
63 * Effect.gen(function* () {
64 * const project = yield* PgProject;
65 * const connection = yield* Hyperdrive;
66 * return CloudflareHyperdrive(connection, {
67 * migrate: project.connectionUri,
68 * });
69 * }),
70 * ),
71 * ),
72 * )
73 * ```
74 *
75 * ### MySQL origins
76 * Hyperdrive also fronts MySQL origins — select the dialect and the layer
77 * drives `mysql2` instead of `pg`.
78 * **Example:** MySQL behind Hyperdrive
79 * ```typescript
80 * CloudflareHyperdrive(connection, {
81 * dialect: "mysql",
82 * migrate: password.connectionUrl,
83 * })
84 * ```
85 *
86 * @layer
87 * @provides BetterAuth.Database
88 * @peer pg
89 * @peer mysql2
90 * @product Hyperdrive
91 */
92export const CloudflareHyperdrive = (
93 connection:
94 | Cloudflare.Hyperdrive.Connection
95 | Effect.Effect<Cloudflare.Hyperdrive.Connection, never, any>,
96 options?: CloudflareHyperdriveOptions,
97) =>
98 Layer.effect(
99 Database,
100 Effect.gen(function* () {
101 const conn = Effect.isEffect(connection)
102 ? yield* connection as Effect.Effect<Cloudflare.Hyperdrive.Connection>
103 : connection;
104 const client = yield* Cloudflare.Hyperdrive.Connect(conn);
105 const source = client.connectionString as Effect.Effect<
106 Redacted.Redacted<string>,
107 never,
108 RuntimeContext
109 >;
110 return options?.dialect === "mysql"
111 ? yield* makeMySQLService(source, {
112 migrate: options?.migrate ?? false,
113 pool: options?.pool as MySQLOptions["pool"],
114 })
115 : yield* makePostgresService(source, {
116 migrate: options?.migrate ?? false,
117 pool: options?.pool as PostgresOptions["pool"],
118 });
119 }),
120 ).pipe(Layer.provide(Cloudflare.Hyperdrive.ConnectBinding));