1---
2title: Drizzle
3description: Generate a Relations v2 auth schema and deploy a Better Auth Lambda with a request-scoped raw Drizzle database.
4---
5
6This 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.
7
8## Install compatible packages
9
10```sh
11bun 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-ab785fc
12bun add -d drizzle-kit@1.0.0-rc.5-ab785fc @types/pg
13```
14
15Keep ORM and Kit versions compatible when upgrading. Configure an [AWS profile](/environments/profiles) and set `DATABASE_URL` to a Postgres database reachable by both your deployer and Lambda.
16
17## Export an upstream instance for the CLI
18
19```typescript title="auth.cli.ts"
20import { drizzleAdapter } from "@better-auth/drizzle-adapter/relations-v2";
21import { betterAuth } from "better-auth";
22import { drizzle } from "drizzle-orm/node-postgres";
23import { Pool } from "pg";
24
25const url = process.env.DATABASE_URL;
26if (!url) throw new Error("DATABASE_URL is required");
27
28const database = drizzle({
29 client: new Pool({ connectionString: url }),
30});
31
32export const auth = betterAuth({
33 basePath: "/api/auth",
34 emailAndPassword: { enabled: true },
35 database: drizzleAdapter(database, { provider: "pg" }),
36});
37```
38
39The 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.
40
41Keep 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.
42
43## Generate the auth schema
44
45Run the CLI with Node.js 22.12 or newer:
46
47```sh
48npx auth@1.7.5 generate --config ./auth.cli.ts --output ./src/auth-schema.ts
49```
50
51This 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.
52
53## Configure and apply migrations
54
55```typescript title="drizzle.config.ts"
56import { defineConfig } from "drizzle-kit";
57
58const url = process.env.DATABASE_URL;
59if (!url) throw new Error("DATABASE_URL is required");
60
61export default defineConfig({
62 dialect: "postgresql",
63 schema: "./src/auth-schema.ts",
64 out: "./drizzle",
65 dbCredentials: { url },
66});
67```
68
69If your application already owns a Drizzle migration history, include the generated auth schema there rather than starting a second history.
70
71```sh
72bunx drizzle-kit generate
73```
74
75Review the generated SQL, especially for an existing database, before applying it:
76
77```sh
78bunx drizzle-kit migrate
79```
80
81Apply migrations before deploying new auth code. This layer never runs Drizzle migrations for you.
82
83## Acquire the database per invocation
84
85```typescript title="src/auth-database.ts"
86import { Drizzle } from "@alchemy.run/better-auth/Drizzle";
87import { drizzle } from "drizzle-orm/node-postgres";
88import * as Config from "effect/Config";
89import * as Effect from "effect/Effect";
90import * as Layer from "effect/Layer";
91import * as Redacted from "effect/Redacted";
92import { Pool } from "pg";
93import * as schema from "./auth-schema.ts";
94
95export const AuthDatabase = Layer.unwrap(
96 Effect.gen(function* () {
97 const url = yield* Config.Redacted("DATABASE_URL");
98 const database = Effect.gen(function* () {
99 const client = yield* Effect.acquireRelease(
100 Effect.sync(() => new Pool({
101 connectionString: Redacted.value(url),
102 max: 1,
103 })),
104 (pool) => Effect.promise(() => pool.end()),
105 );
106 return yield* Effect.sync(() => drizzle({
107 client,
108 relations: schema.authRelations,
109 }));
110 });
111 return Drizzle(database, { provider: "pg", schema });
112 }),
113);
114```
115
116The 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.
117
118## Define the auth Lambda
119
120```typescript title="src/function.ts"
121import { BetterAuth } from "@alchemy.run/better-auth";
122import * as AWS from "alchemy/AWS";
123import * as Duration from "effect/Duration";
124import * as Effect from "effect/Effect";
125import { AuthDatabase } from "./auth-database.ts";
126
127export default class AuthApi extends AWS.Lambda.Function<AuthApi>()(
128 "AuthApi",
129 {
130 main: import.meta.url,
131 functionUrl: true,
132 memorySize: 512,
133 timeout: Duration.seconds(30),
134 build: { install: ["pg"] },
135 },
136 Effect.gen(function* () {
137 const auth = yield* BetterAuth({
138 basePath: "/api/auth",
139 emailAndPassword: { enabled: true },
140 migrate: false,
141 });
142 return { fetch: auth.fetch };
143 }).pipe(Effect.provide(AuthDatabase)),
144) {}
145```
146
147The function exposes `/api/auth` through a public Function URL and packages `pg` in its deployment artifact. Alchemy generates and binds the signing secret automatically.
148
149## Define the stack
150
151```typescript title="alchemy.run.ts"
152import * as Alchemy from "alchemy";
153import * as AWS from "alchemy/AWS";
154import * as Effect from "effect/Effect";
155import AuthApi from "./src/function.ts";
156
157export default Alchemy.Stack(
158 "BetterAuthDrizzle",
159 {
160 providers: AWS.providers(),
161 state: Alchemy.localState(),
162 },
163 Effect.gen(function* () {
164 const api = yield* AuthApi;
165 return { url: api.functionUrl };
166 }),
167);
168```
169
170Preserve 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.
171
172## Deploy and verify
173
174```sh
175bunx alchemy deploy
176```
177
178Set `AUTH_ORIGIN` to the printed Function URL and check the endpoint:
179
180```sh
181curl -i "$AUTH_ORIGIN/api/auth/get-session"
182```
183
184## Existing application relations
185
186When 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.
187
188Drizzle 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.
189
190See [configuration](/better-auth/guides/configuration), [migrations](/better-auth/guides/migrations), the [upgrade guide](/better-auth/upgrades/from-1-6-to-1-7), and the [`Drizzle` reference](/providers/betterauth/drizzle).