| 1 | --- |
| 2 | title: RDS & Aurora |
| 3 | description: 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 | |
| 6 | Aurora is AWS's managed Postgres/MySQL. Bringing it up by hand |
| 7 | means a secret, a subnet group, a cluster, instances, and |
| 8 | optionally 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 |
| 12 | the defaults. |
| 13 | |
| 14 | At runtime there are two ways in: the **`Connect` binding**, |
| 15 | which resolves host, port, and credentials from Secrets Manager |
| 16 | so you can use a normal Postgres driver like `pg`, and the |
| 17 | **Data API** (`AWS.RDSData.*`), which executes SQL over an HTTPS |
| 18 | endpoint with no database connection at all. |
| 19 | |
| 20 | ## Network prerequisite |
| 21 | |
| 22 | Aurora lives inside a VPC. If you don't have one, the |
| 23 | [`Network` helper](/aws/networking) builds the standard |
| 24 | public/private layout in one call; two security groups let the |
| 25 | function reach the database on the Postgres port and nothing |
| 26 | else: |
| 27 | |
| 28 | ```typescript |
| 29 | const network = yield* AWS.EC2.Network("Network", { |
| 30 | cidrBlock: "10.0.0.0/16", |
| 31 | availabilityZones: 2, |
| 32 | nat: "single", |
| 33 | }); |
| 34 | |
| 35 | const functionSecurityGroup = yield* AWS.EC2.SecurityGroup( |
| 36 | "FunctionSecurityGroup", |
| 37 | { |
| 38 | vpcId: network.vpcId, |
| 39 | description: "Security group for the service function", |
| 40 | }, |
| 41 | ); |
| 42 | |
| 43 | const 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 | |
| 61 | See [VPC & networking](/aws/networking) for what `Network` |
| 62 | creates 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 |
| 67 | security groups the database should live in: |
| 68 | |
| 69 | ```typescript |
| 70 | const db = yield* AWS.RDS.Aurora("Database", { |
| 71 | subnetIds: network.privateSubnetIds, |
| 72 | securityGroupIds: [databaseSecurityGroup.groupId], |
| 73 | }); |
| 74 | ``` |
| 75 | |
| 76 | That 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 | |
| 87 | The result exposes every underlying resource — `db.secret`, |
| 88 | `db.subnetGroup`, `db.cluster`, `db.writer`, `db.readers`, |
| 89 | and `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 | |
| 94 | Grow the same call as needs grow: |
| 95 | |
| 96 | ```typescript |
| 97 | const 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 | |
| 119 | The [`Connect`](/providers/aws/rds/connect) binding resolves |
| 120 | connection settings — host, port, database, username, password — |
| 121 | for a cluster, proxy, or proxy endpoint. Credentials are read |
| 122 | from Secrets Manager **at request time**, so they never land in |
| 123 | environment variables, and the binding attaches the |
| 124 | `secretsmanager:GetSecretValue` IAM statement to the function |
| 125 | automatically. |
| 126 | |
| 127 | Wrap 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 |
| 132 | import * as AWS from "alchemy/AWS"; |
| 133 | import * as Context from "effect/Context"; |
| 134 | import * as Data from "effect/Data"; |
| 135 | import * as Effect from "effect/Effect"; |
| 136 | import * as Layer from "effect/Layer"; |
| 137 | import { Client } from "pg"; |
| 138 | import { Network } from "./network.ts"; |
| 139 | |
| 140 | export class SqlError extends Data.TaggedError("SqlError")<{ |
| 141 | readonly message: string; |
| 142 | readonly cause?: unknown; |
| 143 | }> {} |
| 144 | |
| 145 | export 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 | |
| 154 | export 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 |
| 208 | Construction phase and returns an inner Effect; each `yield*` of that |
| 209 | inner Effect at runtime resolves fresh `ConnectionInfo` from the |
| 210 | secret. `Network` here is the same network from the prerequisite |
| 211 | section, wrapped in its own layer (see the |
| 212 | [`examples/aws-rds`](https://github.com/alchemy-run/alchemy/tree/main/examples/aws-rds) |
| 213 | for the full three-file layout). |
| 214 | |
| 215 | The Lambda function provides both layers plus the |
| 216 | `AWS.RDS.ConnectHttp` implementation and serves queries over |
| 217 | HTTP: |
| 218 | |
| 219 | ```typescript |
| 220 | // src/api.ts |
| 221 | import * as AWS from "alchemy/AWS"; |
| 222 | import * as Effect from "effect/Effect"; |
| 223 | import * as Layer from "effect/Layer"; |
| 224 | import * as HttpServerResponse from "effect/http/HttpServerResponse"; |
| 225 | import { Database, DatabaseAurora } from "./database.ts"; |
| 226 | import { NetworkLive } from "./network.ts"; |
| 227 | |
| 228 | export 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 | |
| 253 | The handler only sees `Database` — swap `DatabaseAurora` for a |
| 254 | layer backed by [Neon](/neon) or a local Postgres and nothing |
| 255 | above it changes. |
| 256 | |
| 257 | Yield the function in your Stack as usual: |
| 258 | |
| 259 | ```typescript |
| 260 | // alchemy.run.ts |
| 261 | import * as Alchemy from "alchemy"; |
| 262 | import * as AWS from "alchemy/AWS"; |
| 263 | import * as Effect from "effect/Effect"; |
| 264 | import Api from "./src/api.ts"; |
| 265 | |
| 266 | export 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 | |
| 276 | To reach a private cluster over a direct connection, the Lambda |
| 277 | function also accepts a `vpc` prop |
| 278 | (`vpc: { subnetIds, securityGroupIds }`) that attaches it to the |
| 279 | VPC and adds the `AWSLambdaVPCAccessExecutionRole` managed |
| 280 | policy for you. |
| 281 | |
| 282 | ## The Data API |
| 283 | |
| 284 | Aurora's Data API executes SQL over an HTTPS endpoint — no |
| 285 | connection pool, no driver. The `Aurora` helper enables it by |
| 286 | default, and the `AWS.RDSData` bindings expose it the same way |
| 287 | as any other capability: |
| 288 | |
| 289 | ```typescript |
| 290 | // Construction |
| 291 | const execute = yield* AWS.RDSData.ExecuteStatement(db.cluster, { |
| 292 | secret: db.secret, |
| 293 | database: "app", |
| 294 | }); |
| 295 | |
| 296 | // runtime |
| 297 | const result = yield* execute({ sql: "select now()" }); |
| 298 | ``` |
| 299 | |
| 300 | Provide `AWS.RDSData.ExecuteStatementHttp` as a layer on the |
| 301 | function. The binding attaches both the |
| 302 | `rds-data:ExecuteStatement` and secret-read IAM statements — |
| 303 | no policy JSON to write. |
| 304 | |
| 305 | Transactions and batches follow the same shape: |
| 306 | `BatchExecuteStatement`, `BeginTransaction`, `CommitTransaction`, |
| 307 | and `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 | |
| 330 | Reference: |
| 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) |