1import {
2 resolveConnectionSource,
3 staticConnectionSource,
4 type ConnectionSource,
5} from "alchemy/SQL/ConnectionSource";
6import { openMySQLPool } from "alchemy/SQL/MySQLDriver";
7import * as Effect from "effect/Effect";
8import * as Layer from "effect/Layer";
9import type * as Redacted from "effect/Redacted";
10import type * as Scope from "effect/Scope";
11import type { PoolOptions } from "mysql2/promise";
12import {
13 Database,
14 type DatabaseService,
15 type DirectDatabase,
16} from "./Database.ts";
17import { makeMigrateSupport, type SqlLayerOptions } from "./Postgres.ts";
18
19export interface MySQLOptions extends SqlLayerOptions {
20 readonly pool?: Omit<PoolOptions, "uri">;
21}
22
23/**
24 * Build the MySQL {@link DatabaseService}. Better Auth's Kysely path
25 * expects the `mysql2/promise` pool: it passes the pool itself as the
26 * MysqlDialect config, and the promise pool's `.pool` property is the
27 * callback pool Kysely drives.
28 *
29 * @internal
30 */
31export const makeMySQLService = (
32 source: ConnectionSource,
33 options: MySQLOptions | undefined,
34): Effect.Effect<DatabaseService> =>
35 Effect.gen(function* () {
36 const urlEffect = yield* resolveConnectionSource(source);
37 const migrateSource = staticConnectionSource(source, options?.migrate);
38 const open = (url: Effect.Effect<Redacted.Redacted<string>>) =>
39 openMySQLPool(url, options?.pool) as unknown as Effect.Effect<
40 DirectDatabase,
41 never,
42 Scope.Scope
43 >;
44
45 return {
46 provider: "mysql",
47 runtime: open(urlEffect) as DatabaseService["runtime"],
48 ...(migrateSource === undefined
49 ? {}
50 : {
51 migrate: makeMigrateSupport(
52 migrateSource,
53 open,
54 "Failed to load `mysql2` — install it to migrate a MySQL-backed BetterAuth",
55 ),
56 }),
57 } satisfies DatabaseService;
58 }) as Effect.Effect<DatabaseService>;
59
60/**
61 * Generic TCP MySQL database layer for Better Auth, from a connection
62 * string.
63 *
64 * Works with PlanetScale MySQL, Cloudflare Hyperdrive MySQL origins, AWS
65 * RDS MySQL, or any literal URL. One `mysql2` pool per execution, closed
66 * when the event settles.
67 *
68 *
69 * ### Connecting to PlanetScale MySQL
70 * PlanetScale requires TLS — pass it in the URL's `ssl` query parameter
71 * (mysql2 parses it as JSON).
72 * **Example:** PlanetScale MySQL with TLS
73 * ```typescript
74 * import { BetterAuth } from "@alchemy.run/better-auth";
75 * import { MySQL } from "@alchemy.run/better-auth/MySQL";
76 *
77 * const password = yield* Planetscale.MySQLPassword("auth-db", {
78 * database,
79 * role: "admin",
80 * });
81 * const url =
82 * `mysql://${password.username}:...@${password.host}/${database.name}` +
83 * `?ssl=${encodeURIComponent('{"rejectUnauthorized":true}')}`;
84 *
85 * Effect.gen(function* () {
86 * const auth = yield* BetterAuth({ emailAndPassword: { enabled: true } });
87 * return { fetch: ... };
88 * }).pipe(Effect.provide(MySQL(url)))
89 * ```
90 *
91 * @layer
92 * @provides BetterAuth.Database
93 * @peer mysql2
94 * @product MySQL
95 */
96export const MySQL = (
97 url: ConnectionSource,
98 options?: MySQLOptions,
99): Layer.Layer<Database> =>
100 Layer.effect(
101 Database,
102 makeMySQLService(url, options),
103 ) as Layer.Layer<Database>;