1---
2title: Postgres
3description: Deploy Better Auth on Lambda with an existing PostgreSQL database and a request-scoped pg pool.
4---
5
6This example serves Better Auth through a Lambda Function URL using an existing PostgreSQL database. Both the Lambda and the deploying machine must be able to reach the server over TCP.
7
8## Install
9
10```sh
11bun add @alchemy.run/better-auth better-auth@^1.7.5 kysely pg
12```
13
14Configure AWS through an [Alchemy profile](/environments/profiles) and set `DATABASE_URL` in the deploying environment. The connection layer does not create the database, credentials, or network access.
15
16## Define the database layer
17
18```typescript title="src/auth-database.ts"
19import { Postgres } from "@alchemy.run/better-auth/Postgres";
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 Postgres(url);
28 }),
29);
30```
31
32Configuration resolves during host construction and is bound into the Lambda environment. The pool is not opened until an auth operation runs, and closes at the end of the invocation.
33
34## Define the auth Lambda
35
36```typescript title="src/function.ts"
37import { BetterAuth } from "@alchemy.run/better-auth";
38import * as AWS from "alchemy/AWS";
39import * as Duration from "effect/Duration";
40import * as Effect from "effect/Effect";
41import { AuthDatabase } from "./auth-database.ts";
42
43export default class AuthApi extends AWS.Lambda.Function<AuthApi>()(
44 "AuthApi",
45 {
46 main: import.meta.url,
47 functionUrl: true,
48 memorySize: 512,
49 timeout: Duration.seconds(30),
50 build: { install: ["pg"] },
51 },
52 Effect.gen(function* () {
53 const auth = yield* BetterAuth({
54 basePath: "/api/auth",
55 emailAndPassword: { enabled: true },
56 });
57 return { fetch: auth.fetch };
58 }).pipe(Effect.provide(AuthDatabase)),
59) {}
60```
61
62`build.install` packages the dynamically imported `pg` driver with a conventional npm layout. `functionUrl: true` creates a public endpoint; Better Auth handles authentication under `/api/auth`.
63
64## Define the stack
65
66```typescript title="alchemy.run.ts"
67import * as Alchemy from "alchemy";
68import * as AWS from "alchemy/AWS";
69import * as Effect from "effect/Effect";
70import AuthApi from "./src/function.ts";
71
72export default Alchemy.Stack(
73 "BetterAuthPostgres",
74 {
75 providers: AWS.providers(),
76 state: Alchemy.localState(),
77 },
78 Effect.gen(function* () {
79 const api = yield* AuthApi;
80 return { url: api.functionUrl };
81 }),
82);
83```
84
85Alchemy generates and binds the signing secret automatically. Preserve and protect `.alchemy` state to retain it across deployments.
86
87## Deploy and migrate
88
89```sh
90bunx alchemy deploy
91```
92
93Automatic migrations use the resolved URL before updating the function, so the deployer needs schema-changing permissions. The runtime pool defaults to one connection per invocation; migrations create auth tables, not the database itself.
94
95Set `AUTH_ORIGIN` to the printed Function URL to verify the endpoint:
96
97```sh
98curl -i "$AUTH_ORIGIN/api/auth/get-session"
99```
100
101## Use separate migration credentials
102
103For a restricted runtime role or a pooled runtime URL, set `DATABASE_MIGRATION_URL` as well and replace `src/auth-database.ts`:
104
105```typescript title="src/auth-database.ts"
106import { Postgres } from "@alchemy.run/better-auth/Postgres";
107import * as Config from "effect/Config";
108import * as Effect from "effect/Effect";
109import * as Layer from "effect/Layer";
110
111export const AuthDatabase = Layer.unwrap(
112 Effect.gen(function* () {
113 const url = yield* Config.Redacted("DATABASE_URL");
114 const migrationUrl = yield* Config.Redacted("DATABASE_MIGRATION_URL");
115 return Postgres(url, { migrate: migrationUrl });
116 }),
117);
118```
119
120Both configuration values are bound to the host because they are read during construction. For migration credentials that must remain deploy-only, supply a resource Output as `migrate` rather than reading privileged credentials through host configuration.
121
122## Network and host requirements
123
124The Lambda above assumes a server reachable without VPC attachment; private RDS requires Lambda VPC connectivity and security-group access. Configure TLS according to your database provider, without disabling certificate verification.
125
126`Postgres` also accepts connection-string resource Outputs, such as PlanetScale's `role.connectionUrl`. For Workers, prefer [Hyperdrive](/better-auth/databases/cloudflare-hyperdrive) or [Neon](/better-auth/databases/neon); direct TCP requires `nodejs_compat` and a reachable origin.
127
128Changing the connection does not transfer auth records, and automatic migrations are additive. See [configuration](/better-auth/guides/configuration), [migrations](/better-auth/guides/migrations), the [upgrade guide](/better-auth/upgrades/from-1-6-to-1-7), and the [`Postgres` reference](/providers/betterauth/postgres).