Skip to main content
Reserved number: 0473, assigned to batch stackra-254 (STA-1409) for the data-layer wave 3 round of 2026-10-28. It closes 0238 implementation notes 2 and 3.

Context

0238 gave every service its own logical database on one server per environment, and left pooling open: note 2 named a transaction-mode proxy and a pool budget per deployable group in runway.yaml, and note 3 deferred the budget sizes until the first deployable existed. That is now. Runway already accepts database.poolBudget per service (and defaults.service.database.poolBudget at the root), but nothing read it: PostgresDatabaseDriver.connect left MikroORM’s pool at its defaults, so every instance could open ten connections whatever the topology scaled it to. The migration and seed runners hold a session advisory lock and session SETs, which a transaction-mode proxy does not keep, and they had only the pooled DATABASE_URL. The owner answered on 2026-10-28 (data-layer decision 5): a managed proxy per cloud.

Decision

  1. The proxy is the cloud’s managed one. On AWS the runtime connects through RDS Proxy; on GCP through the Cloud SQL connector, dialling the instance connection name gcpRuntime exports. No self-run PgBouncer. The proxy is a deployment fact: application code never knows whether one stands in front.
  2. The budget is per service; the pool is per instance. database.poolBudget in a service runway (else the root’s defaults.service.database.poolBudget) is the most connections all instances of that service hold together on its database. One instance’s pool max is the budget divided by the most instances its topology group may run, rounded down (poolMaxOf in @stackra/runway; one instance for a monolith, a split member or an isolated one unless a group states more). A budget below the instance count is refused, since an instance would get no connection. The deploy hands the share to the process as DATABASE_POOL_MAX; the generated boot passes DatabaseConfig.pool with that value over its max, and PostgresDatabaseDriver.connect({ pool }) sizes the primary and each replica with it. Without a budget the pool is DatabaseConfig.pool (the pg defaults: ten, none kept, ten seconds idle).
  3. Session tools go direct. A tool that holds session state (the migration runner’s advisory lock and timeouts, the seed runner’s SETs, any CLI like them) connects with directDatabaseUrl() from @stackra/database/node: DATABASE_DIRECT_URL, the server past the proxy, else DATABASE_URL when no proxy stands in front (local, test containers). The runtime never uses the direct URL.
  4. Scope stays transaction-local. The scope settings stay set_config(..., true) inside the action transaction (0136), which a transaction-mode proxy keeps; no runtime code uses a session SET. This confirms 0238 note 1.

Consequences

  • The server’s connection limit is planned per environment as the sum of the service budgets plus the direct sessions of migrations and seeds; scaling a group up shrinks each instance’s pool instead of exhausting the server.
  • RDS Proxy pins a session that issues a session SET or an advisory lock, which is why decision 3 keeps those off it.
  • The GCP connector runs inside the service process, so GCP needs no proxy resource; AWS needs one.
  • On AWS the RDS instance is Encore Cloud’s, so the RDS Proxy cannot be created by @stackra/infra-aws today (the same limit as pgaudit, data-layer decision 6). Until it exists, both URLs reach the server and each instance keeps to its share of the budget on its own.

Enforcement

poolMaxOf tests (budget divided by instances, the root default, the refusal); poolConfig and DatabaseConfig.pool tests; the emitBoot test for DATABASE_POOL_MAX; the directDatabaseUrl test, and both runners read it (stackra-254). planned — the deploy provider setting DATABASE_POOL_MAX and DATABASE_DIRECT_URL per service, and the RDS Proxy on AWS.

Supersedes and sources

Supersedes: none. Closes 0238 implementation notes 2 and 3. Related: 0159 (the runner’s session timeouts), 0136. Sources: owner answers to the data-layer questionnaire, 2026-10-28 (decision 5); notes handover/gap-analysis/data-layer/database-migrations.md (gap 11) and scale-performance.md (gap 6); Linear STA-1409.