| 1 | --- |
| 2 | title: Drizzle + Aurora PostgreSQL |
| 3 | description: 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 |
| 11 | git clone https://github.com/alchemy-run/alchemy.git |
| 12 | cd alchemy |
| 13 | pnpm install |
| 14 | cd examples/aws-aurora-drizzle |
| 15 | ``` |
| 16 | |
| 17 | The 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 |
| 20 | Aurora is billable until destroyed. This example is disposable: destroy deletes its database data. |
| 21 | ::: |
| 22 | |
| 23 | ## Provision a private database |
| 24 | |
| 25 | ```typescript |
| 26 | const 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 |
| 47 | import { bootstrap } from "./bootstrap.ts"; |
| 48 | |
| 49 | const schemaVersion = yield* bootstrap; |
| 50 | ``` |
| 51 | |
| 52 | The [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 |
| 57 | const 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 | |
| 66 | Provide `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 | |
| 70 | Set the Lambda environment: |
| 71 | |
| 72 | ```typescript |
| 73 | env: { NODE_EXTRA_CA_CERTS: "/var/runtime/ca-cert.pem" } |
| 74 | ``` |
| 75 | |
| 76 | The RDS binding currently emits `sslmode=no-verify`. Override it to require certificate and hostname verification: |
| 77 | |
| 78 | ```typescript |
| 79 | import * as Drizzle from "alchemy/Drizzle/Postgres"; |
| 80 | import * as Effect from "effect/Effect"; |
| 81 | import * as Redacted from "effect/Redacted"; |
| 82 | |
| 83 | const 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 | |
| 94 | The 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 |
| 99 | pnpm 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 |
| 101 | pnpm add -D @types/pg |
| 102 | ``` |
| 103 | |
| 104 | ```typescript |
| 105 | build: { 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 |
| 113 | const 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 |
| 121 | pnpm deploy --profile testing --stage aurora-example |
| 122 | aws 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 | |
| 128 | Use 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 | |
| 132 | The 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 |
| 137 | pnpm add -D drizzle-kit@1.0.0-rc.5-ab785fc |
| 138 | ``` |
| 139 | |
| 140 | ```typescript |
| 141 | // drizzle.config.ts |
| 142 | import { defineConfig } from "drizzle-kit"; |
| 143 | |
| 144 | export default defineConfig({ |
| 145 | dialect: "postgresql", |
| 146 | schema: "./src/schema.ts", |
| 147 | out: "./migrations", |
| 148 | }); |
| 149 | ``` |
| 150 | |
| 151 | Generation 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 |
| 156 | pnpm exec drizzle-kit generate --config=drizzle.config.ts |
| 157 | ``` |
| 158 | |
| 159 | Run 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 |
| 164 | git add src/schema.ts drizzle.config.ts migrations/ |
| 165 | git diff --cached -- src/schema.ts drizzle.config.ts migrations/ |
| 166 | git commit -m "feat(db): update Aurora schema and migration" |
| 167 | ``` |
| 168 | |
| 169 | Review 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 |
| 175 | import { defineConfig } from "drizzle-kit"; |
| 176 | import config from "./drizzle.config.ts"; |
| 177 | |
| 178 | const url = process.env.DATABASE_URL; |
| 179 | if (!url) throw new Error("DATABASE_URL is required for migrations"); |
| 180 | |
| 181 | export default defineConfig({ |
| 182 | ...config, |
| 183 | dbCredentials: { url }, |
| 184 | }); |
| 185 | ``` |
| 186 | |
| 187 | Inject 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 |
| 192 | pnpm exec drizzle-kit migrate --config=drizzle.migrate.config.ts |
| 193 | ``` |
| 194 | |
| 195 | Run 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 |
| 200 | AWS_TEST_SLOW=1 ALCHEMY_PROFILE=testing bun test test/integ.test.ts |
| 201 | ``` |
| 202 | |
| 203 | This deploys real AWS resources and destroys them afterward; provisioning and cleanup take several minutes. |
| 204 | |
| 205 | ## Clean up |
| 206 | |
| 207 | ```sh |
| 208 | pnpm destroy --profile testing --stage aurora-example |
| 209 | ``` |
| 210 | |
| 211 | Wait 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) |