MySQL
This example connects a Better Auth Lambda to an existing MySQL database over TCP with TLS. The database, credentials, and network access must already exist.
Install
Section titled “Install”pnpm add https://pkg.alchemy.run/@alchemy.run/better-auth/pr:2930:51c662a better-auth@^1.7.5 kysely mysql2bun add https://pkg.alchemy.run/@alchemy.run/better-auth/pr:2930:51c662a better-auth@^1.7.5 kysely mysql2npm install https://pkg.alchemy.run/@alchemy.run/better-auth/pr:2930:51c662a better-auth@^1.7.5 kysely mysql2yarn add https://pkg.alchemy.run/@alchemy.run/better-auth/pr:2930:51c662a better-auth@^1.7.5 kysely mysql2Configure AWS through an Alchemy profile and set DATABASE_URL in the deploying environment. Both the deployer and Lambda must be able to reach that URL.
Define the database layer
Section titled “Define the database layer”import { MySQL } from "@alchemy.run/better-auth/MySQL";import * as Config from "effect/Config";import * as Effect from "effect/Effect";import * as Layer from "effect/Layer";
export const AuthDatabase = Layer.unwrap( Effect.gen(function* () { const url = yield* Config.Redacted("DATABASE_URL"); return MySQL(url, { pool: { ssl: { rejectUnauthorized: true } }, }); }),);The 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.
Define the auth Lambda
Section titled “Define the auth Lambda”import { BetterAuth } from "@alchemy.run/better-auth";import * as AWS from "@alchemy.run/aws";import * as Duration from "effect/Duration";import * as Effect from "effect/Effect";import { AuthDatabase } from "./auth-database.ts";
export default class AuthApi extends AWS.Lambda.Function<AuthApi>()( "AuthApi", { main: import.meta.url, functionUrl: true, memorySize: 512, timeout: Duration.seconds(30), build: { install: ["mysql2"] }, }, Effect.gen(function* () { const auth = yield* BetterAuth({ basePath: "/api/auth", emailAndPassword: { enabled: true }, }); return { fetch: auth.fetch }; }).pipe(Effect.provide(AuthDatabase)),) {}The 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.
Define the stack
Section titled “Define the stack”import * as Alchemy from "alchemy";import * as AWS from "@alchemy.run/aws";import * as Effect from "effect/Effect";import AuthApi from "./src/function.ts";
export default Alchemy.Stack( "BetterAuthMySQL", { providers: AWS.providers(), state: Alchemy.localState(), }, Effect.gen(function* () { const api = yield* AuthApi; return { url: api.functionUrl }; }),);Preserve 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.
Deploy and migrate
Section titled “Deploy and migrate”pnpm alchemy deploybun alchemy deploynpx alchemy deployyarn alchemy deployAutomatic 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.
Set AUTH_ORIGIN to the printed Function URL and check the endpoint:
curl -i "$AUTH_ORIGIN/api/auth/get-session"Separate migration permissions
Section titled “Separate migration permissions”Set DATABASE_MIGRATION_URL alongside the runtime URL and replace src/auth-database.ts with this file:
import { MySQL } from "@alchemy.run/better-auth/MySQL";import * as Config from "effect/Config";import * as Effect from "effect/Effect";import * as Layer from "effect/Layer";
export const AuthDatabase = Layer.unwrap( Effect.gen(function* () { const url = yield* Config.Redacted("DATABASE_URL"); const migrationUrl = yield* Config.Redacted("DATABASE_MIGRATION_URL"); return MySQL(url, { migrate: migrationUrl, pool: { ssl: { rejectUnauthorized: true } }, }); }),);Configuration 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.
Other hosts and existing schemas
Section titled “Other hosts and existing schemas”For Workers, prefer Hyperdrive with a MySQL origin and nodejs_compat. Changing adapters does not transfer users or sessions between databases.
Review MySQL index and string-length constraints in the 1.6 to 1.7 upgrade guide before upgrading a populated database. See configuration, migrations, and the MySQL reference.