1---
2title: RDS & Aurora
3description: Stand up an Aurora cluster in one call with the Aurora helper, connect from Lambda over the Connect binding with pg, or skip connections entirely with the Data API.
4---
5
6Aurora is AWS's managed Postgres/MySQL. Bringing it up by hand
7means a secret, a subnet group, a cluster, instances, and
8optionally a proxy — alchemy wraps all of that in the
9**`Aurora` helper**, one call that returns every underlying
10`DB*` resource so you can drop down to the canonical surface
11(`DBCluster`, `DBInstance`, `DBProxy`, …) whenever you outgrow
12the defaults.
13
14At runtime there are two ways in: the **`Connect` binding**,
15which resolves host, port, and credentials from Secrets Manager
16so you can use a normal Postgres driver like `pg`, and the
17**Data API** (`AWS.RDSData.*`), which executes SQL over an HTTPS
18endpoint with no database connection at all.
19
20## Network prerequisite
21
22Aurora lives inside a VPC. If you don't have one, the
23[`Network` helper](/aws/networking) builds the standard
24public/private layout in one call; two security groups let the
25function reach the database on the Postgres port and nothing
26else:
27
28```typescript
29const network = yield* AWS.EC2.Network("Network", {
30 cidrBlock: "10.0.0.0/16",
31 availabilityZones: 2,
32 nat: "single",
33});
34
35const functionSecurityGroup = yield* AWS.EC2.SecurityGroup(
36 "FunctionSecurityGroup",
37 {
38 vpcId: network.vpcId,
39 description: "Security group for the service function",
40 },
41);
42
43const databaseSecurityGroup = yield* AWS.EC2.SecurityGroup(
44 "DatabaseSecurityGroup",
45 {
46 vpcId: network.vpcId,
47 description: "Security group for the Aurora cluster",
48 ingress: [
49 {
50 ipProtocol: "tcp",
51 fromPort: 5432,
52 toPort: 5432,
53 referencedGroupId: functionSecurityGroup.groupId,
54 description: "Allow the function to reach Aurora PostgreSQL",
55 },
56 ],
57 },
58);
59```
60
61See [VPC & networking](/aws/networking) for what `Network`
62creates and how to build the same layout by hand.
63
64## The Aurora helper
65
66[`Aurora`](/providers/aws/rds/aurora) needs only the subnets and
67security groups the database should live in:
68
69```typescript
70const db = yield* AWS.RDS.Aurora("Database", {
71 subnetIds: network.privateSubnetIds,
72 securityGroupIds: [databaseSecurityGroup.groupId],
73});
74```
75
76That one call creates:
77
78- a **Secrets Manager secret** with a generated 32-character
79 password for the admin user (`app` by default),
80- a **DB subnet group** over the subnets you passed,
81- an **Aurora PostgreSQL cluster** (`aurora-postgresql`) with a
82 database named `app`, storage snapshots tagged, and the
83 **Data API enabled** (`dataApi: false` to turn it off),
84- one **Serverless v2 writer** instance scaling between 0.5 and
85 1 ACU.
86
87The result exposes every underlying resource — `db.secret`,
88`db.subnetGroup`, `db.cluster`, `db.writer`, `db.readers`,
89and `db.proxy` when enabled — so expanding into the low-level
90`AWS.RDS.DB*` surface never means rewriting the stack.
91
92## Readers, scaling, and a proxy
93
94Grow the same call as needs grow:
95
96```typescript
97const db = yield* AWS.RDS.Aurora("Database", {
98 subnetIds: network.privateSubnetIds,
99 securityGroupIds: [databaseSecurityGroup.groupId],
100 readers: 2,
101 proxy: true,
102 scaling: { minCapacity: 0.5, maxCapacity: 4 },
103});
104```
105
106- `readers` adds read replicas alongside the writer, with
107 promotion tiers set so failover order is deterministic.
108- `proxy: true` wires up an RDS Proxy — the IAM role that reads
109 the secret, the proxy itself (TLS required by default), and a
110 target group pointed at the cluster. Pass an object instead of
111 `true` to tune auth, the target group, or add an extra
112 endpoint.
113- `scaling` sets the Serverless v2 ACU range; for provisioned
114 instances set `instanceClass: "db.r6g.large"` instead of the
115 default `db.serverless`.
116
117## Connect from Lambda with pg
118
119The [`Connect`](/providers/aws/rds/connect) binding resolves
120connection settings — host, port, database, username, password —
121for a cluster, proxy, or proxy endpoint. Credentials are read
122from Secrets Manager **at request time**, so they never land in
123environment variables, and the binding attaches the
124`secretsmanager:GetSecretValue` IAM statement to the function
125automatically.
126
127Wrap it in an Effect service so the rest of your code depends on
128`query`, not on Aurora:
129
130```typescript
131// src/database.ts
132import * as AWS from "alchemy/AWS";
133import * as Context from "effect/Context";
134import * as Data from "effect/Data";
135import * as Effect from "effect/Effect";
136import * as Layer from "effect/Layer";
137import { Client } from "pg";
138import { Network } from "./network.ts";
139
140export class SqlError extends Data.TaggedError("SqlError")<{
141 readonly message: string;
142 readonly cause?: unknown;
143}> {}
144
145export class Database extends Context.Service<
146 Database,
147 {
148 query(
149 statement: string,
150 ): Effect.Effect<ReadonlyArray<Record<string, unknown>>, SqlError>;
151 }
152>()("Database") {}
153
154export const DatabaseAurora = Layer.effect(
155 Database,
156 Effect.gen(function* () {
157 const { vpc, databaseSecurityGroup } = yield* Network;
158
159 const db = yield* AWS.RDS.Aurora("Database", {
160 subnetIds: vpc.privateSubnetIds,
161 securityGroupIds: [databaseSecurityGroup.groupId],
162 });
163
164 const connect = yield* AWS.RDS.Connect(db.cluster, {
165 secret: db.secret,
166 database: "app",
167 });
168
169 return Database.of({
170 query: (statement) =>
171 connect.pipe(
172 Effect.catch((cause) =>
173 Effect.fail(
174 new SqlError({ message: "Failed to resolve connection", cause }),
175 ),
176 ),
177 Effect.flatMap((connection) =>
178 Effect.tryPromise({
179 try: async () => {
180 const client = new Client({
181 host: connection.host,
182 port: connection.port,
183 database: connection.database,
184 user: connection.username,
185 password: connection.password,
186 ssl: connection.ssl
187 ? { rejectUnauthorized: false }
188 : undefined,
189 });
190 await client.connect();
191 try {
192 return (await client.query(statement)).rows;
193 } finally {
194 await client.end();
195 }
196 },
197 catch: (cause) =>
198 new SqlError({ message: `Failed to execute: ${statement}`, cause }),
199 }),
200 ),
201 ),
202 });
203 }),
204);
205```
206
207`yield* AWS.RDS.Connect(db.cluster, ...)` runs in the function's
208Construction phase and returns an inner Effect; each `yield*` of that
209inner Effect at runtime resolves fresh `ConnectionInfo` from the
210secret. `Network` here is the same network from the prerequisite
211section, wrapped in its own layer (see the
212[`examples/aws-rds`](https://github.com/alchemy-run/alchemy/tree/main/examples/aws-rds)
213for the full three-file layout).
214
215The Lambda function provides both layers plus the
216`AWS.RDS.ConnectHttp` implementation and serves queries over
217HTTP:
218
219```typescript
220// src/api.ts
221import * as AWS from "alchemy/AWS";
222import * as Effect from "effect/Effect";
223import * as Layer from "effect/Layer";
224import * as HttpServerResponse from "effect/http/HttpServerResponse";
225import { Database, DatabaseAurora } from "./database.ts";
226import { NetworkLive } from "./network.ts";
227
228export default class Api extends AWS.Lambda.Function<Api>()(
229 "Api",
230 { main: import.meta.url, runtime: "nodejs24.x" },
231 Effect.gen(function* () {
232 const db = yield* Database;
233
234 return {
235 fetch: db.query("select now()::text as now").pipe(
236 Effect.flatMap((rows) => HttpServerResponse.json(rows[0] ?? null)),
237 Effect.catchTag("SqlError", (error) =>
238 HttpServerResponse.json({ error: error.message }, { status: 500 }),
239 ),
240 ),
241 };
242 }).pipe(
243 Effect.provide(
244 Layer.provideMerge(
245 Layer.mergeAll(DatabaseAurora),
246 Layer.mergeAll(NetworkLive, AWS.RDS.ConnectHttp),
247 ),
248 ),
249 ),
250) {}
251```
252
253The handler only sees `Database` — swap `DatabaseAurora` for a
254layer backed by [Neon](/neon) or a local Postgres and nothing
255above it changes.
256
257Yield the function in your Stack as usual:
258
259```typescript
260// alchemy.run.ts
261import * as Alchemy from "alchemy";
262import * as AWS from "alchemy/AWS";
263import * as Effect from "effect/Effect";
264import Api from "./src/api.ts";
265
266export default Alchemy.Stack(
267 "MyApp",
268 { providers: AWS.providers(), state: AWS.state() },
269 Effect.gen(function* () {
270 const api = yield* Api;
271 return { url: api.functionUrl };
272 }),
273);
274```
275
276To reach a private cluster over a direct connection, the Lambda
277function also accepts a `vpc` prop
278(`vpc: { subnetIds, securityGroupIds }`) that attaches it to the
279VPC and adds the `AWSLambdaVPCAccessExecutionRole` managed
280policy for you.
281
282## The Data API
283
284Aurora's Data API executes SQL over an HTTPS endpoint — no
285connection pool, no driver. The `Aurora` helper enables it by
286default, and the `AWS.RDSData` bindings expose it the same way
287as any other capability:
288
289```typescript
290// Construction
291const execute = yield* AWS.RDSData.ExecuteStatement(db.cluster, {
292 secret: db.secret,
293 database: "app",
294});
295
296// runtime
297const result = yield* execute({ sql: "select now()" });
298```
299
300Provide `AWS.RDSData.ExecuteStatementHttp` as a layer on the
301function. The binding attaches both the
302`rds-data:ExecuteStatement` and secret-read IAM statements —
303no policy JSON to write.
304
305Transactions and batches follow the same shape:
306`BatchExecuteStatement`, `BeginTransaction`, `CommitTransaction`,
307and `RollbackTransaction`, each with a matching `*Http` layer.
308
309## Where next
310
311- [Drizzle + Aurora PostgreSQL](/aws/data/drizzle-aurora) — a standalone Lambda
312 application with IAM database authentication, verified TLS, and controlled schema setup.
313- [Drizzle + Aurora DSQL](/aws/data/drizzle-dsql) — a different database with a
314 public IAM-authenticated endpoint, no Aurora PostgreSQL cluster or VPC required.
315
316- [SQL databases](/sql/databases) — database engines, connection paths, and client choices.
317
318
319- [VPC & networking](/aws/networking) — the `Network` helper and
320 the primitives underneath it.
321- [Lambda](/aws/compute/lambda) — the runtime this page binds Aurora
322 into.
323- [Secrets & env](/aws/security/secrets-env) — how the generated secret
324 and GetSecretValue binding this page relies on actually work.
325- [Neon](/neon) — serverless Postgres with branching, no VPC
326 required.
327- [PlanetScale](/planetscale) — horizontally sharded MySQL and
328 Postgres.
329
330Reference:
331
332- [`Aurora` reference](/providers/aws/rds/aurora) — every prop
333 on the helper.
334- [`Connect` reference](/providers/aws/rds/connect)
335- [`ExecuteStatement` reference](/providers/aws/rdsdata/executestatement)