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.
Install
Section titled “Install”bun add @alchemy.run/better-auth better-auth@^1.7.5 kysely @distilled.cloud/awsConfigure AWS through an Alchemy profile in a region supporting Aurora PostgreSQL’s Data API. No pg driver or TCP connection pool is required.
Define the database and network
Section titled “Define the database and network”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.
Define the auth Lambda
Section titled “Define the auth Lambda”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.
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( "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.
Deploy and migrate
Section titled “Deploy and migrate”bunx alchemy deployAutomatic 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:
curl -i "$AUTH_ORIGIN/api/auth/get-session"Cluster and migration options
Section titled “Cluster and migration options”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.