1---
2title: MySQL
3description: Deploy Better Auth on Lambda with an existing MySQL database and a request-scoped mysql2 pool.
4---
5
6This example connects a Better Auth Lambda to an existing MySQL database over TCP with TLS. The database, credentials, and network access must already exist.
7
8## Install
9
10```sh
11bun add @alchemy.run/better-auth better-auth@^1.7.5 kysely mysql2
12```
13
14Configure AWS through an [Alchemy profile](/environments/profiles) and set `DATABASE_URL` in the deploying environment. Both the deployer and Lambda must be able to reach that URL.
15
16## Define the database layer
17
18```typescript title="src/auth-database.ts"
19import { MySQL } from "@alchemy.run/better-auth/MySQL";
20import * as Config from "effect/Config";
21import * as Effect from "effect/Effect";
22import * as Layer from "effect/Layer";
23
24export const AuthDatabase = Layer.unwrap(
25 Effect.gen(function* () {
26 const url = yield* Config.Redacted("DATABASE_URL");
27 return MySQL(url, {
28 pool: { ssl: { rejectUnauthorized: true } },
29 });
30 }),
31);
32```
33
34The URL is bound during host construction; the `mysql2` pool opens per invocation and closes when it settles. The TLS configuration suits hosted services such as PlanetScale; provide your database's CA when it is not covered by the system trust store.
35
36## Define the auth Lambda
37
38```typescript title="src/function.ts"
39import { BetterAuth } from "@alchemy.run/better-auth";
40import * as AWS from "alchemy/AWS";
41import * as Duration from "effect/Duration";
42import * as Effect from "effect/Effect";
43import { AuthDatabase } from "./auth-database.ts";
44
45export default class AuthApi extends AWS.Lambda.Function<AuthApi>()(
46 "AuthApi",
47 {
48 main: import.meta.url,
49 functionUrl: true,
50 memorySize: 512,
51 timeout: Duration.seconds(30),
52 build: { install: ["mysql2"] },
53 },
54 Effect.gen(function* () {
55 const auth = yield* BetterAuth({
56 basePath: "/api/auth",
57 emailAndPassword: { enabled: true },
58 });
59 return { fetch: auth.fetch };
60 }).pipe(Effect.provide(AuthDatabase)),
61) {}
62```
63
64The function exposes Better Auth at `/api/auth` through a public Function URL. The driver is installed in the deployment artifact, and the signing secret is generated and bound automatically.
65
66## Define the stack
67
68```typescript title="alchemy.run.ts"
69import * as Alchemy from "alchemy";
70import * as AWS from "alchemy/AWS";
71import * as Effect from "effect/Effect";
72import AuthApi from "./src/function.ts";
73
74export default Alchemy.Stack(
75 "BetterAuthMySQL",
76 {
77 providers: AWS.providers(),
78 state: Alchemy.localState(),
79 },
80 Effect.gen(function* () {
81 const api = yield* AuthApi;
82 return { url: api.functionUrl };
83 }),
84);
85```
86
87Preserve and protect the local `.alchemy` state directory across deployments. The example assumes network access without VPC attachment; a private database also needs Lambda VPC and security-group configuration.
88
89## Deploy and migrate
90
91```sh
92bunx alchemy deploy
93```
94
95Automatic migrations use the resolved URL and the same pool options before updating the function. The deployer needs schema-changing permissions; migrations create auth tables inside the existing database.
96
97Set `AUTH_ORIGIN` to the printed Function URL and check the endpoint:
98
99```sh
100curl -i "$AUTH_ORIGIN/api/auth/get-session"
101```
102
103## Separate migration permissions
104
105Set `DATABASE_MIGRATION_URL` alongside the runtime URL and replace `src/auth-database.ts` with this file:
106
107```typescript title="src/auth-database.ts"
108import { MySQL } from "@alchemy.run/better-auth/MySQL";
109import * as Config from "effect/Config";
110import * as Effect from "effect/Effect";
111import * as Layer from "effect/Layer";
112
113export const AuthDatabase = Layer.unwrap(
114 Effect.gen(function* () {
115 const url = yield* Config.Redacted("DATABASE_URL");
116 const migrationUrl = yield* Config.Redacted("DATABASE_MIGRATION_URL");
117 return MySQL(url, {
118 migrate: migrationUrl,
119 pool: { ssl: { rejectUnauthorized: true } },
120 });
121 }),
122);
123```
124
125Configuration read during host construction is bound into its environment, including this migration URL. Use a deploy-only resource Output instead when the runtime must not receive privileged migration credentials.
126
127## Other hosts and existing schemas
128
129For Workers, prefer [Hyperdrive](/better-auth/databases/cloudflare-hyperdrive) with a MySQL origin and `nodejs_compat`. Changing adapters does not transfer users or sessions between databases.
130
131Review MySQL index and string-length constraints in the [1.6 to 1.7 upgrade guide](/better-auth/upgrades/from-1-6-to-1-7) before upgrading a populated database. See [configuration](/better-auth/guides/configuration), [migrations](/better-auth/guides/migrations), and the [`MySQL` reference](/providers/betterauth/mysql).