1---
2title: Drizzle + Aurora PostgreSQL
3description: Drizzle on Lambda with private Aurora PostgreSQL, IAM authentication, verified TLS, and reviewed SQL migrations.
4---
5
6[Runnable example](https://github.com/alchemy-run/alchemy/tree/main/examples/aws-aurora-drizzle) · [AWS setup](/aws/setup) · [Aurora DSQL instead](/aws/data/drizzle-dsql)
7
8## Prerequisites
9
10```sh
11git clone https://github.com/alchemy-run/alchemy.git
12cd alchemy
13pnpm install
14cd examples/aws-aurora-drizzle
15```
16
17The example uses `us-west-2`, Aurora PostgreSQL 17.5, and Node.js 22. Change both subnet availability zones if you change region. Application queries use Effect-native Drizzle over `pg`, not the RDS Data API driver; [SQL databases](/sql/databases) compares the integrations.
18
19:::caution
20Aurora is billable until destroyed. This example is disposable: destroy deletes its database data.
21:::
22
23## Provision a private database
24
25```typescript
26const aurora = yield* AWS.RDS.Aurora("Database", {
27 databaseName: "app",
28 engine: "aurora-postgresql",
29 engineVersion: "17.5",
30 subnetIds,
31 securityGroupIds: [dbSecurityGroup.groupId],
32 secret: { username: "dbadmin" },
33 dataApi: true,
34 cluster: {
35 enableIAMDatabaseAuthentication: true,
36 serverlessV2ScalingConfiguration: { MinCapacity: 0.5, MaxCapacity: 1 },
37 },
38 instance: { dbInstanceClass: "db.serverless", publiclyAccessible: false },
39});
40```
41
42[Network setup](https://github.com/alchemy-run/alchemy/blob/main/examples/aws-aurora-drizzle/src/database.ts) allows port 5432 only from the Lambda's security group. [Aurora options](/aws/data/rds).
43
44## Initialize the schema during deployment
45
46```typescript
47import { bootstrap } from "./bootstrap.ts";
48
49const schemaVersion = yield* bootstrap;
50```
51
52The [deployment Action](https://github.com/alchemy-run/alchemy/blob/main/examples/aws-aurora-drizzle/src/bootstrap.ts) creates `todos` and the restricted `app_iam` login through the Data API. This is initial setup only, not a versioned migration runner. The application role gets table CRUD but cannot create objects in `public`.
53
54## Bind the application identity
55
56```typescript
57const connect = yield* AWS.RDS.Connect(aurora.cluster, {
58 auth: "iam",
59 username: "app_iam",
60 database: "app",
61 subnetIds,
62 securityGroupIds: [lambdaSecurityGroup.groupId],
63});
64```
65
66Provide `AWS.RDS.ConnectHttp` on the Lambda Effect. The binding attaches Lambda to the VPC and grants `rds-db:connect` for `app_iam`; runtime authentication signs an IAM token rather than reading the administrator secret. [Complete handler](https://github.com/alchemy-run/alchemy/blob/main/examples/aws-aurora-drizzle/src/Api.ts).
67
68## Verify the database certificate
69
70Set the Lambda environment:
71
72```typescript
73env: { NODE_EXTRA_CA_CERTS: "/var/runtime/ca-cert.pem" }
74```
75
76The RDS binding currently emits `sslmode=no-verify`. Override it to require certificate and hostname verification:
77
78```typescript
79import * as Drizzle from "alchemy/Drizzle/Postgres";
80import * as Effect from "effect/Effect";
81import * as Redacted from "effect/Redacted";
82
83const db = yield* Drizzle.Postgres(
84 connect.pipe(
85 Effect.map((connection) => {
86 const url = new URL(Redacted.value(connection.url));
87 url.searchParams.set("sslmode", "verify-full");
88 return Redacted.make(url.toString());
89 }),
90 ),
91);
92```
93
94The CA path is specific to [managed Lambda Node.js runtimes](https://docs.aws.amazon.com/lambda/latest/dg/lambda-nodejs.html#nodejs-certificate-loading). The connection stays lazy: the first query resolves the redacted URL and opens a pool scoped to that invocation, which closes when its scope ends.
95
96## Package the driver
97
98```sh
99pnpm add alchemy effect@4.0.0-rc.115 @effect/sql-pg@4.0.0-rc.115 \
100 drizzle-orm@1.0.0-rc.5-ab785fc pg
101pnpm add -D @types/pg
102```
103
104```typescript
105build: { install: ["pg"] }
106```
107
108`build.install` keeps the CommonJS `pg` package intact in the Lambda artifact.
109
110## Query with Drizzle
111
112```typescript
113const rows = yield* db.select().from(todos);
114```
115
116[Schema and queries](/sql/drizzle/postgres) · [Connection lifecycle](/sql/effect-sql/lifecycle)
117
118## Deploy and invoke
119
120```sh
121pnpm deploy --profile testing --stage aurora-example
122aws lambda invoke --region us-west-2 \
123 --function-name '<functionName>' \
124 --cli-binary-format raw-in-base64-out \
125 --payload file://health-event.json response.json
126```
127
128Use the printed function name. The Function URL requires [SigV4-signed requests](https://github.com/alchemy-run/alchemy/blob/main/examples/aws-aurora-drizzle/test/integ.test.ts).
129
130## Evolve the schema
131
132The example's bootstrap does not alter existing tables or maintain Drizzle migration history. Before adopting generated migrations for an already-bootstrapped database, establish a reviewed baseline matching the existing schema; do not replay an initial `CREATE TABLE` migration over it.
133
134### Configure SQL generation
135
136```sh
137pnpm add -D drizzle-kit@1.0.0-rc.5-ab785fc
138```
139
140```typescript
141// drizzle.config.ts
142import { defineConfig } from "drizzle-kit";
143
144export default defineConfig({
145 dialect: "postgresql",
146 schema: "./src/schema.ts",
147 out: "./migrations",
148});
149```
150
151Generation compares the [schema module](https://github.com/alchemy-run/alchemy/blob/main/examples/aws-aurora-drizzle/src/schema.ts) with local snapshots and does not need a database connection.
152
153### Generate after a schema change
154
155```sh
156pnpm exec drizzle-kit generate --config=drizzle.config.ts
157```
158
159Run this whenever `src/schema.ts` changes, before deployment. Do not put SQL generation in a deploy hook.
160
161### Review and commit the migration
162
163```sh
164git add src/schema.ts drizzle.config.ts migrations/
165git diff --cached -- src/schema.ts drizzle.config.ts migrations/
166git commit -m "feat(db): update Aurora schema and migration"
167```
168
169Review the staged SQL, snapshots, destructive operations, and rename decisions before committing. Commit the entire generated migration directory with the schema. The release process consumes committed SQL; deployment must not generate or silently rewrite it.
170
171### Configure the migration connection
172
173```typescript
174// drizzle.migrate.config.ts
175import { defineConfig } from "drizzle-kit";
176import config from "./drizzle.config.ts";
177
178const url = process.env.DATABASE_URL;
179if (!url) throw new Error("DATABASE_URL is required for migrations");
180
181export default defineConfig({
182 ...config,
183 dbCredentials: { url },
184});
185```
186
187Inject a migration-role PostgreSQL URL for the `app` database through the runner's secret store, using `sslmode=verify-full` and the RDS CA trust bundle. The runner needs its own network route and security-group access to the private cluster; Lambda's binding grants neither to a laptop or CI job. Keep this DDL-capable identity separate from `app_iam`.
188
189### Apply the committed SQL
190
191```sh
192pnpm exec drizzle-kit migrate --config=drizzle.migrate.config.ts
193```
194
195Run from the authorized migration runner before releasing code that requires the new schema. Keep an existing `drizzle-kit migrate` workflow; `alchemy deploy` is not its replacement. Neither `AWS.RDS.Aurora` nor `AWS.RDS.DBCluster` accepts a `migrations` prop, and the example's Action only performs its explicit bootstrap SQL.
196
197## Run the integration test
198
199```sh
200AWS_TEST_SLOW=1 ALCHEMY_PROFILE=testing bun test test/integ.test.ts
201```
202
203This deploys real AWS resources and destroys them afterward; provisioning and cleanup take several minutes.
204
205## Clean up
206
207```sh
208pnpm destroy --profile testing --stage aurora-example
209```
210
211Wait for deletion to finish. Reuse the same profile and stage if cleanup is interrupted.
212
213## Next steps
214
215[SQL databases](/sql/databases) · [Aurora configuration](/aws/data/rds) · [Lambda](/aws/compute/lambda)