1import { drizzleAdapter } from "@better-auth/drizzle-adapter/relations-v2";
2import type { RuntimeContext } from "alchemy";
3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";
5import * as Scope from "effect/Scope";
6import { Database, type DatabaseInput, type Provider } from "./Database.ts";
7
8type DrizzleDatabase = Parameters<typeof drizzleAdapter>[0];
9type DrizzleDatabaseEffect = Effect.Effect<
10 DrizzleDatabase,
11 never,
12 RuntimeContext | Scope.Scope
13>;
14
15const isDatabaseEffect = (
16 value: DrizzleDatabase | DrizzleDatabaseEffect,
17): value is DrizzleDatabaseEffect => Effect.isEffect(value);
18
19export interface DrizzleLayerConfig {
20 /** The SQL dialect of the underlying drizzle database. */
21 readonly provider: "pg" | "mysql" | "sqlite";
22 /**
23 * The drizzle schema containing the Better Auth tables (generate it with
24 * `npx auth@1.7.5 generate`). When omitted, the adapter resolves tables
25 * off the db instance's registered schema.
26 */
27 readonly schema?: Record<string, unknown>;
28 /** @default false */
29 readonly usePlural?: boolean;
30 /** @default false (snake_case column names) */
31 readonly camelCase?: boolean;
32}
33
34/**
35 * Use an existing Drizzle database as Better Auth's storage via
36 * better-auth's Relations v2 `drizzleAdapter`.
37 *
38 * Accepts a plain drizzle instance, or an Effect resolving to one for
39 * databases that only materialize at runtime. NOTE: alchemy's own
40 * `Drizzle.Postgres`/`Drizzle.D1` chainable proxies yield Effects rather
41 * than thenables and cannot back the (promise-based) adapter — pass the
42 * raw `drizzle(...)` instance instead.
43 *
44 * Schema management is yours: this layer has no automatic migration
45 * support (`npx auth@1.7.5 generate` + your drizzle-kit flow own the
46 * tables). Spread generated `authRelations` after your app's
47 * `defineRelations` when constructing the db. The CLI configuration must
48 * use `drizzleAdapter` from `@better-auth/drizzle-adapter/relations-v2`.
49 * Run `npx auth@1.7.5 generate --config ./auth.cli.ts`.
50 * Passing `--adapter drizzle` selects the CLI's legacy Relations v1 generator.
51 *
52 * ### Bringing your own Drizzle db
53 * **Example:** Postgres drizzle instance with generated auth relations
54 * ```typescript
55 * import { BetterAuth } from "@alchemy.run/better-auth";
56 * import { Drizzle } from "@alchemy.run/better-auth/Drizzle";
57 * import { drizzle } from "drizzle-orm/node-postgres";
58 * import * as schema from "./auth-schema.ts";
59 * import { relations } from "./app-schema.ts";
60 *
61 * const db = drizzle({
62 * client: pool,
63 * relations: { ...relations, ...schema.authRelations },
64 * });
65 *
66 * Effect.gen(function* () {
67 * const auth = yield* BetterAuth({ emailAndPassword: { enabled: true } });
68 * // ...
69 * }).pipe(Effect.provide(Drizzle(db, { provider: "pg", schema })))
70 * ```
71 *
72 * @layer
73 * @provides BetterAuth.Database
74 * @peer drizzle-orm
75 * @peer @better-auth/drizzle-adapter
76 * @product Drizzle
77 */
78export const Drizzle = (
79 db: DrizzleDatabase | DrizzleDatabaseEffect,
80 config: DrizzleLayerConfig,
81): Layer.Layer<Database> =>
82 Layer.sync(Database, () => ({
83 provider: (config.provider === "pg"
84 ? "postgres"
85 : config.provider) as Provider,
86 runtime: Effect.gen(function* () {
87 const database = isDatabaseEffect(db) ? yield* db : db;
88 return drizzleAdapter(database, {
89 provider: config.provider,
90 ...(config.schema !== undefined ? { schema: config.schema } : {}),
91 ...(config.usePlural !== undefined
92 ? { usePlural: config.usePlural }
93 : {}),
94 ...(config.camelCase !== undefined
95 ? { camelCase: config.camelCase }
96 : {}),
97 }) as DatabaseInput;
98 }),
99 }));