1import type { Input, RuntimeContext } from "alchemy";
2import type { BetterAuthOptions } from "better-auth";
3import * as Context from "effect/Context";
4import type * as Effect from "effect/Effect";
5import type * as Scope from "effect/Scope";
6import type { BetterAuthMigrationError } from "./Errors.ts";
7
8/** The database dialect family, as Better Auth's Kysely adapter names them. */
9export type Provider = "sqlite" | "postgres" | "mysql";
10
11/**
12 * Everything Better Auth accepts as `options.database`: a raw driver object
13 * (pg `Pool`, mysql2 pool, `bun:sqlite` `Database`, Cloudflare `D1Database`,
14 * ...), a Kysely dialect form, or an adapter factory
15 * (`drizzleAdapter(...)`, `memoryAdapter(...)`, custom adapters).
16 */
17export type DatabaseInput = NonNullable<BetterAuthOptions["database"]>;
18
19/**
20 * The Kysely-resolvable subset of {@link DatabaseInput} — raw driver objects
21 * and dialect forms, excluding adapter factories.
22 *
23 * This is the only shape `migrate.connect` may yield: Better Auth's
24 * `getMigrations` supports exactly these forms and hard-`process.exit(1)`s
25 * on adapter factories, so excluding them statically keeps that landmine
26 * unreachable.
27 */
28export type DirectDatabase = Exclude<
29 DatabaseInput,
30 (...args: never[]) => unknown
31>;
32
33export interface DatabaseService {
34 /** Dialect family of the database this layer connects to. */
35 readonly provider: Provider;
36 /**
37 * Build the runtime `options.database` input. Evaluated once per
38 * execution (memoized on the execution Scope by {@link BetterAuth}), so
39 * disposable resources acquired with `Effect.acquireRelease` — pg/mysql
40 * pools — are closed when the event settles. That is the only legal
41 * pooling shape on workerd (sockets are IoContext-pinned).
42 *
43 * Deploy/plan evaluation never runs this effect: construction is deferred
44 * to the first `api`/`fetch`/`getSession` use inside an execution scope.
45 */
46 readonly runtime: Effect.Effect<
47 DatabaseInput,
48 never,
49 RuntimeContext | Scope.Scope
50 >;
51 /**
52 * Deploy-time migration support. `undefined` means this platform cannot
53 * auto-migrate (memory, drizzle, custom adapters): {@link BetterAuth}
54 * skips migration registration, and fails synth if `migrate: true` was
55 * explicitly requested.
56 */
57 readonly migrate?: {
58 /**
59 * Non-secret, canonically-hashable identity of the target database
60 * (resource Outputs allowed) — part of the migration Action's input, so
61 * migrations re-run when the target database is replaced.
62 *
63 * MUST NOT contain the connection string: Action input is persisted to
64 * alchemy state.
65 */
66 readonly identity: Input<Record<string, unknown>>;
67 /**
68 * Two-stage connection for the migration Action.
69 *
70 * The OUTER effect is the Action's *init* half: it runs at stack-eval
71 * time under the Action's capture context — this is where resource
72 * Outputs (`database.databaseId`, a connection-string attr) MUST be
73 * yielded so the engine records them as dependency captures and
74 * resolves them at apply. No I/O here.
75 *
76 * The INNER effect is the *apply* half: it runs inside the Action body
77 * with the resolved captures ambient, acquires the actual client/pool
78 * on the Scope (`pool.end` / `db.close` ride it), and yields a
79 * Kysely-resolvable database.
80 */
81 readonly connect: Effect.Effect<
82 Effect.Effect<DirectDatabase, BetterAuthMigrationError, Scope.Scope>,
83 never,
84 RuntimeContext
85 >;
86 };
87}
88
89/**
90 * The database dependency of {@link BetterAuth}.
91 *
92 * Platform layers (`Memory`, `SQLite`, `CloudflareD1`, `Postgres`, `MySQL`,
93 * `Drizzle`, ...) provide this service; `yield* BetterAuth({...})` consumes
94 * it. Provide the layer on the surrounding impl effect:
95 *
96 * ```typescript
97 * Effect.gen(function* () {
98 * const auth = yield* BetterAuth({ emailAndPassword: { enabled: true } });
99 * return { fetch: ... };
100 * }).pipe(Effect.provide(CloudflareD1(Db)))
101 * ```
102 */
103export class Database extends Context.Service<Database, DatabaseService>()(
104 "BetterAuth.Database",
105) {}