Skip to Content
API (apps/api)Database Access

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:connect for its role on the Proxy (in AppApiStack — see /infra).
  • @aws-sdk/rds-signer mints a ~15-minute auth token; pg’s password option 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

PoolPostgres roleWho can use itPurpose
getPool()opsdna_appevery HTTP LambdaRLS-scoped tenant traffic
getIdentityWriterPool()identity_writeridentity-resolve Lambda onlyidentity-resolution writes (OPS-362, migration 0334 write gate)
getSourceDocumentMaintenancePool()source-doc maintenance rolethe scheduled source-doc workerscross-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 runtime

The 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.

Last updated on