| 1 | import type { RuntimeContext } from "alchemy"; |
| 2 | import * as Cloudflare from "alchemy/Cloudflare"; |
| 3 | import * as Effect from "effect/Effect"; |
| 4 | import * as Layer from "effect/Layer"; |
| 5 | import type * as Redacted from "effect/Redacted"; |
| 6 | import { Database } from "./Database.ts"; |
| 7 | import { makeMySQLService, type MySQLOptions } from "./MySQL.ts"; |
| 8 | import { |
| 9 | makePostgresService, |
| 10 | type MigrateSource, |
| 11 | type PostgresOptions, |
| 12 | } from "./Postgres.ts"; |
| 13 | |
| 14 | export 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 | */ |
| 92 | export 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)); |