Database access from Lambdas
All database access in apps/api flows through apps/api/src/rds.ts: Lambda → RDS Proxy → RDS Postgres, authenticated with short-lived IAM tokens instead of passwords, and connected as one of three Postgres roles depending on what the Lambda is allowed to see.
IAM auth and pooling
- The Lambda’s execution role is granted
rds-db:connectfor its role on the Proxy (inAppApiStack— see /infra). @aws-sdk/rds-signermints a ~15-minute auth token;pg’spasswordoption is an async function, invoked once per new connection, so fresh tokens appear exactly when needed. Reused connections keep their original token — fine, because RDS Proxy holds the upstream connection regardless.- The pool is module-scoped per warm container (
max: 4,idleTimeoutMillis: 60_000). It only amortizes the handshake; RDS Proxy does the real pooling.
Three role-scoped pools
| Pool | Postgres role | Who can use it | Purpose |
|---|---|---|---|
getPool() | opsdna_app | every HTTP Lambda | RLS-scoped tenant traffic |
getIdentityWriterPool() | identity_writer | identity-resolve Lambda only | identity-resolution writes (OPS-362, migration 0334 write gate) |
getSourceDocumentMaintenancePool() | source-doc maintenance role | the scheduled source-doc workers | cross-tenant sweeps with no tenant predicate (OPS-424) |
The isolation is double-locked: opsdna_app is deliberately not a member of identity_writer (no SET ROLE bypass), and only the designated Lambdas’ execution roles hold rds-db:connect for the privileged users — so the privileged pools cannot even be constructed elsewhere.
Environment configuration
readRdsAppEnv() requires all of:
RDS_PROXY_ENDPOINT # hostname only, e.g. opsdna-proxy.proxy-abc123def.us-west-2.rds.amazonaws.com
RDS_DATABASE_NAME # e.g. opsdna
RDS_APP_USER # e.g. opsdna_app
AWS_REGION # set automatically by the Lambda runtimeThe privileged pools additionally require RDS_IDENTITY_WRITER_USER or RDS_SOURCE_DOCUMENT_MAINTENANCE_USER.
Missing env vars throw at cold start. readRdsAppEnv() (and its identity-writer / maintenance siblings) throw a plain Error on first pool use if any variable is unset — every invocation of that Lambda then 500s until the env is fixed in AppApiStack.
Never pass search_path as a libpq startup option. RDS Proxy rejects command-line startup options at connect time; an options: '-c search_path=...' change took down every proxy connection on 2026-07-05 (#1242, reverted in #1282). installRdsProxySafeSearchPathAndInstrumentation instead runs SET search_path TO ... on each connection checkout — proxy-safe, and pinned for the pooled connection’s lifetime. It also wraps pool.query and each checked-out client’s query to feed per-query timings into the request-phase diagnostics.
Effect layer
For Effect-composed code there is RdsPoolService / RdsPoolLive — an acquireRelease layer that builds the pool from rdsPoolEffectConfig and closes it on scope end, mapping construction failures to a PersistenceError (kind: 'configuration').
Handlers should not run SQL against these pools directly — they go through @opsdna/persistence repositories; see /persistence for the repository and RLS-tenant-context patterns.