1import {
2 resolveConnectionSource,
3 staticConnectionSource,
4 type ConnectionSource,
5} from "alchemy/SQL/ConnectionSource";
6import * as Effect from "effect/Effect";
7import * as Layer from "effect/Layer";
8import * as Redacted from "effect/Redacted";
9import type * as Scope from "effect/Scope";
10import {
11 Database,
12 type DatabaseService,
13 type DirectDatabase,
14} from "./Database.ts";
15import { makeMigrateSupport, type SqlLayerOptions } from "./Postgres.ts";
16
17export interface NeonOptions extends SqlLayerOptions {}
18
19// The Neon serverless Pool is pg-compatible (Better Auth's Kysely path
20// duck-types it as a Postgres pool via its `connect` method) but speaks
21// WebSocket instead of TCP — no `pg`, no Hyperdrive, no nodejs socket
22// APIs. Loaded dynamically so the driver stays an optional peer.
23const loadNeonPool = Effect.promise(
24 () => import("@neondatabase/serverless"),
25).pipe(
26 Effect.map((mod) =>
27 (mod as { default?: { Pool?: unknown } }).default?.Pool !== undefined
28 ? (
29 mod as unknown as {
30 default: {
31 Pool: new (config: {
32 connectionString: string;
33 max?: number;
34 }) => unknown;
35 };
36 }
37 ).default.Pool
38 : (
39 mod as unknown as {
40 Pool: new (config: {
41 connectionString: string;
42 max?: number;
43 }) => unknown;
44 }
45 ).Pool,
46 ),
47);
48
49const openPool = (
50 urlEffect: Effect.Effect<Redacted.Redacted<string>>,
51): Effect.Effect<DirectDatabase, never, Scope.Scope> =>
52 Effect.gen(function* () {
53 const Pool = yield* loadNeonPool;
54 const url = Redacted.value(yield* urlEffect);
55 return (yield* Effect.acquireRelease(
56 Effect.sync(() => new Pool({ connectionString: url, max: 1 })),
57 (pool) => Effect.promise(() => (pool as { end(): Promise<void> }).end()),
58 )) as DirectDatabase;
59 });
60
61/**
62 * Neon database layer for Better Auth over Neon's serverless driver.
63 *
64 * This is the optimal Workers/Lambda → Neon pairing: the driver speaks
65 * WebSocket instead of TCP, so it needs no Hyperdrive, no `pg` install,
66 * and no socket compatibility flags. One pool per execution, closed when
67 * the event settles.
68 *
69 * For TCP access through Cloudflare Hyperdrive use `CloudflareHyperdrive`;
70 * for a generic `pg` connection use `Postgres`.
71 *
72 *
73 * ### Connecting from a Worker or Lambda
74 * The `connectionUri` Output binds into the host environment at deploy
75 * and is read back at runtime; the same source drives the deploy-time
76 * migration Action.
77 * **Example:** Worker (or Lambda) with a Neon-backed BetterAuth
78 * ```typescript
79 * import { BetterAuth } from "@alchemy.run/better-auth";
80 * import { Neon as NeonDatabase } from "@alchemy.run/better-auth/Neon";
81 * import * as Neon from "alchemy/Neon";
82 *
83 * export const AuthDb = Neon.Project("AuthDb");
84 *
85 * Effect.gen(function* () {
86 * const auth = yield* BetterAuth({ emailAndPassword: { enabled: true } });
87 * return { fetch: ... };
88 * }).pipe(
89 * Effect.provide(
90 * Layer.unwrap(Effect.map(AuthDb, (db) => NeonDatabase(db.connectionUri))),
91 * ),
92 * )
93 * ```
94 *
95 * ### Branch-per-stage setups
96 * Point the layer at a branch's connection string instead of the project's
97 * to isolate auth data per stage.
98 * **Example:** Using a Neon branch
99 * ```typescript
100 * const branch = yield* Neon.Branch("auth-db", { project });
101 * // ...
102 * Effect.provide(Layer.unwrap(Effect.map(branch, (b) => NeonDatabase(b.connectionUri))))
103 * ```
104 *
105 * @layer
106 * @provides BetterAuth.Database
107 * @peer @neondatabase/serverless
108 * @product Neon
109 */
110export const Neon = (
111 url: ConnectionSource,
112 options?: NeonOptions,
113): Layer.Layer<Database> =>
114 Layer.effect(
115 Database,
116 Effect.gen(function* () {
117 const urlEffect = yield* resolveConnectionSource(url);
118 const migrateSource = staticConnectionSource(url, options?.migrate);
119
120 return {
121 provider: "postgres",
122 runtime: openPool(urlEffect) as DatabaseService["runtime"],
123 ...(migrateSource === undefined
124 ? {}
125 : {
126 migrate: makeMigrateSupport(
127 migrateSource,
128 openPool,
129 "Failed to connect to Neon for Better Auth schema migrations",
130 ),
131 }),
132 } satisfies DatabaseService;
133 }),
134 ) as Layer.Layer<Database>;