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