Skip to content

Aurora Data API

This example creates Aurora PostgreSQL and serves Better Auth through a Lambda Function URL. Aurora needs a VPC, but Lambda reaches the Data API over HTTPS without joining that VPC.

Terminal window
bun add @alchemy.run/better-auth better-auth@^1.7.5 kysely @distilled.cloud/aws

Configure AWS through an Alchemy profile in a region supporting Aurora PostgreSQL’s Data API. No pg driver or TCP connection pool is required.

src/database.ts
import * as AWS from "alchemy/AWS";
import * as Effect from "effect/Effect";
export const AuthDb = Effect.gen(function* () {
const network = yield* AWS.EC2.Network("AuthNetwork", {
cidrBlock: "10.42.0.0/16",
availabilityZones: 2,
});
const securityGroup = yield* AWS.EC2.SecurityGroup("AuthSecurityGroup", {
vpcId: network.vpcId,
description: "Aurora auth database",
});
return yield* AWS.RDS.Aurora("AuthDb", {
databaseName: "auth",
subnetIds: network.privateSubnetIds,
securityGroupIds: [securityGroup.groupId],
dataApi: true,
});
});

The Aurora composite creates the cluster, writer, and database-credentials secret. The security group needs no inbound Postgres rule for Data API access.

src/function.ts
import { BetterAuth } from "@alchemy.run/better-auth";
import { AuroraDataApi } from "@alchemy.run/better-auth/AuroraDataApi";
import * as AWS from "alchemy/AWS";
import * as Duration from "effect/Duration";
import * as Effect from "effect/Effect";
import { AuthDb } from "./database.ts";
export default class AuthApi extends AWS.Lambda.Function<AuthApi>()(
"AuthApi",
{
main: import.meta.url,
functionUrl: true,
memorySize: 512,
timeout: Duration.seconds(30),
},
Effect.gen(function* () {
const auth = yield* BetterAuth({
basePath: "/api/auth",
emailAndPassword: { enabled: true },
});
return { fetch: auth.fetch };
}).pipe(Effect.provide(AuroraDataApi(AuthDb, { database: "auth" }))),
) {}

The database layer binds cluster and secret identifiers and grants the function Data API and Secrets Manager permissions. The public Function URL serves auth endpoints 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(
"BetterAuthAurora",
{
providers: AWS.providers(),
state: Alchemy.localState(),
},
Effect.gen(function* () {
const api = yield* AuthApi;
return { url: api.functionUrl };
}),
);

The auth signing secret is generated and bound automatically, separately from Aurora’s database-credentials secret. Preserve and protect local .alchemy state across deployments.

Terminal window
bunx alchemy deploy

Automatic migrations wait for the writer and use the Data API with deploy-time AWS credentials. Those credentials need database and secret access too; the function’s IAM bindings do not grant permissions to the deployer.

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

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

For a bare AWS.RDS.DBCluster resource, provide an AWS.SecretsManager.Secret resource through the layer’s secret option. Passing the Aurora composite, as above, also supplies the writer dependency needed to order migrations.

The layer retries transient errors while a Serverless v2 cluster resumes, with bounded retries at deployment and runtime. SQL runs within each invocation without retaining a TCP connection across invocations.

The layer’s migrate: false option disables automatic migration support; Better Auth’s own migrate: false also opts out of its migration Action. Automatic migrations are additive and do not move records between databases.

See configuration, migrations, the upgrade guide, and the AuroraDataApi reference.