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 inrunway.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
- 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
gcpRuntimeexports. No self-run PgBouncer. The proxy is a deployment fact: application code never knows whether one stands in front. - The budget is per service; the pool is per instance.
database.poolBudgetin a service runway (else the root’sdefaults.service.database.poolBudget) is the most connections all instances of that service hold together on its database. One instance’s poolmaxis the budget divided by the most instances its topology group may run, rounded down (poolMaxOfin@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 asDATABASE_POOL_MAX; the generated boot passesDatabaseConfig.poolwith that value over itsmax, andPostgresDatabaseDriver.connect({ pool })sizes the primary and each replica with it. Without a budget the pool isDatabaseConfig.pool(thepgdefaults: ten, none kept, ten seconds idle). - 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 withdirectDatabaseUrl()from@stackra/database/node:DATABASE_DIRECT_URL, the server past the proxy, elseDATABASE_URLwhen no proxy stands in front (local, test containers). The runtime never uses the direct URL. - 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 sessionSET. 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
SETor 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-awstoday (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); noteshandover/gap-analysis/data-layer/database-migrations.md (gap 11) and
scale-performance.md (gap 6); Linear STA-1409.