Skip to content

Postgres

This 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.

Terminal window
bun add @alchemy.run/better-auth better-auth@^1.7.5 kysely pg

Configure AWS through an Alchemy profile and set DATABASE_URL in the deploying environment. The connection layer does not create the database, credentials, or network access.

src/auth-database.ts
import { Postgres } from "@alchemy.run/better-auth/Postgres";
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 Postgres(url);
}),
);

Configuration 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.

src/function.ts
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 },
});
return { fetch: auth.fetch };
}).pipe(Effect.provide(AuthDatabase)),
) {}

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.

alchemy.run.ts
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(
"BetterAuthPostgres",
{
providers: AWS.providers(),
state: Alchemy.localState(),
},
Effect.gen(function* () {
const api = yield* AuthApi;
return { url: api.functionUrl };
}),
);

Alchemy generates and binds the signing secret automatically. Preserve and protect .alchemy state to retain it across deployments.

Terminal window
bunx alchemy deploy

Automatic 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.

Set AUTH_ORIGIN to the printed Function URL to verify the endpoint:

Terminal window
curl -i "$AUTH_ORIGIN/api/auth/get-session"

For a restricted runtime role or a pooled runtime URL, set DATABASE_MIGRATION_URL as well and replace src/auth-database.ts:

src/auth-database.ts
import { Postgres } from "@alchemy.run/better-auth/Postgres";
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 Postgres(url, { migrate: migrationUrl });
}),
);

Both 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.

The 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.

Postgres also accepts connection-string resource Outputs, such as PlanetScale’s role.connectionUrl. For Workers, prefer Hyperdrive or Neon; direct TCP requires nodejs_compat and a reachable origin.

Changing the connection does not transfer auth records, and automatic migrations are additive. See configuration, migrations, the upgrade guide, and the Postgres reference.