Drizzle
This example uses an existing Postgres database and serves Better Auth through a Lambda Function URL. Drizzle owns the schema and migration history; the Better Auth layer takes a raw drizzle(...) database, not an Alchemy Drizzle proxy that returns Effects.
Install compatible packages
Section titled “Install compatible packages”bun add @alchemy.run/better-auth better-auth@^1.7.5 @better-auth/drizzle-adapter@^1.7.5 pg drizzle-orm@1.0.0-rc.5-ab785fcbun add -d drizzle-kit@1.0.0-rc.5-ab785fc @types/pgKeep ORM and Kit versions compatible when upgrading. Configure an AWS profile and set DATABASE_URL to a Postgres database reachable by both your deployer and Lambda.
Export an upstream instance for the CLI
Section titled “Export an upstream instance for the CLI”import { drizzleAdapter } from "@better-auth/drizzle-adapter/relations-v2";import { betterAuth } from "better-auth";import { drizzle } from "drizzle-orm/node-postgres";import { Pool } from "pg";
const url = process.env.DATABASE_URL;if (!url) throw new Error("DATABASE_URL is required");
const database = drizzle({ client: new Pool({ connectionString: url }),});
export const auth = betterAuth({ basePath: "/api/auth", emailAndPassword: { enabled: true }, database: drizzleAdapter(database, { provider: "pg" }),});The CLI needs upstream betterAuth, not an Effect or a Context service. This CLI-only pool is never imported by the Lambda; the CLI instance is for schema generation, not serving requests.
Keep plugins, additional fields, and model names aligned between this call and the Lambda’s BetterAuth call below. Alchemy-only id and migrate options belong only in the runtime call.
Generate the auth schema
Section titled “Generate the auth schema”Run the CLI with Node.js 22.12 or newer:
npx auth@1.7.5 generate --config ./auth.cli.ts --output ./src/auth-schema.tsThis command creates the src/auth-schema.ts imported below, including auth tables and authRelations. Do not pass --adapter drizzle: it selects the legacy Relations v1 generator rather than the configured Relations v2 adapter.
Configure and apply migrations
Section titled “Configure and apply migrations”import { defineConfig } from "drizzle-kit";
const url = process.env.DATABASE_URL;if (!url) throw new Error("DATABASE_URL is required");
export default defineConfig({ dialect: "postgresql", schema: "./src/auth-schema.ts", out: "./drizzle", dbCredentials: { url },});If your application already owns a Drizzle migration history, include the generated auth schema there rather than starting a second history.
bunx drizzle-kit generateReview the generated SQL, especially for an existing database, before applying it:
bunx drizzle-kit migrateApply migrations before deploying new auth code. This layer never runs Drizzle migrations for you.
Acquire the database per invocation
Section titled “Acquire the database per invocation”import { Drizzle } from "@alchemy.run/better-auth/Drizzle";import { drizzle } from "drizzle-orm/node-postgres";import * as Config from "effect/Config";import * as Effect from "effect/Effect";import * as Layer from "effect/Layer";import * as Redacted from "effect/Redacted";import { Pool } from "pg";import * as schema from "./auth-schema.ts";
export const AuthDatabase = Layer.unwrap( Effect.gen(function* () { const url = yield* Config.Redacted("DATABASE_URL"); const database = Effect.gen(function* () { const client = yield* Effect.acquireRelease( Effect.sync(() => new Pool({ connectionString: Redacted.value(url), max: 1, })), (pool) => Effect.promise(() => pool.end()), ); return yield* Effect.sync(() => drizzle({ client, relations: schema.authRelations, })); }); return Drizzle(database, { provider: "pg", schema }); }),);The URL resolves during host construction, while the inner Effect opens and releases the pool per invocation. Do not move new Pool to runtime module scope or layer construction.
Define the auth Lambda
Section titled “Define the auth Lambda”import { BetterAuth } from "@alchemy.run/better-auth";import * as AWS from "alchemy/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: ["pg"] }, }, Effect.gen(function* () { const auth = yield* BetterAuth({ basePath: "/api/auth", emailAndPassword: { enabled: true }, migrate: false, }); return { fetch: auth.fetch }; }).pipe(Effect.provide(AuthDatabase)),) {}The function exposes /api/auth through a public Function URL and packages pg in its deployment artifact. Alchemy generates and binds the signing secret automatically.
Define the stack
Section titled “Define the stack”import * as Alchemy from "alchemy";import * as AWS from "alchemy/AWS";import * as Effect from "effect/Effect";import AuthApi from "./src/function.ts";
export default Alchemy.Stack( "BetterAuthDrizzle", { providers: AWS.providers(), state: Alchemy.localState(), }, Effect.gen(function* () { const api = yield* AuthApi; return { url: api.functionUrl }; }),);Preserve and protect local .alchemy state across deployments. This example assumes a reachable Postgres origin; private databases also require Lambda VPC connectivity and security-group access.
Deploy and verify
Section titled “Deploy and verify”bunx alchemy deploySet AUTH_ORIGIN to the printed Function URL and check the endpoint:
curl -i "$AUTH_ORIGIN/api/auth/get-session"Existing application relations
Section titled “Existing application relations”When combining schemas, spread your application’s Relations v2 defineRelations result before the generated authRelations in the raw database’s relations option. Keep the generated auth tables in the adapter’s schema option too; legacy relations() objects are not interchangeable.
Drizzle has no wrapper-managed migration support, so leave Better Auth’s migrate unset or false; setting it to true fails. On Workers, keep the same per-request acquisition pattern, enable nodejs_compat, and choose a driver and origin reachable from workerd.
See configuration, migrations, the upgrade guide, and the Drizzle reference.