Skip to content

feat(db): resolve DATABASE_URL per role (DATABASE_URL_<ROLE> with fallback)#5276

Merged
TheodoreSpeaks merged 2 commits into
stagingfrom
feat/db-url-per-role
Jun 30, 2026
Merged

feat(db): resolve DATABASE_URL per role (DATABASE_URL_<ROLE> with fallback)#5276
TheodoreSpeaks merged 2 commits into
stagingfrom
feat/db-url-per-role

Conversation

@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator

Summary

  • @sim/db now resolves its connection URL per runtime role: DATABASE_URL_<ROLE> (e.g. DATABASE_URL_TRIGGER) when set, falling back to the existing DATABASE_URL — same for the replica (DATABASE_REPLICA_URL_<ROLE>).
  • No-op until the keyed vars are populated — with none set, behavior is identical to today. This is the inert, reversible switch for the in-progress connection-pool-isolation work (separate Postgres users + dedicated PgBouncers per surface).
  • New shared helper resolveDbUrl(base, role) in packages/db/connection-url.ts, consumed by both packages/db/db.ts (primary + replica) and the realtime socketDb (apps/realtime/.../operations.ts) — so both realtime pools land on the realtime URL when set.
  • Role is the existing SIM_DB_ROLE selector (already merged), so the URL and the pool-size profile always agree.
  • Declared the new optional env vars in the app + realtime env schemas (typing/docs; @sim/db reads process.env directly).

How the cutover works

Provision the users + bouncers (inert), ship this (no-op), then flip one surface at a time by populating its keyed var + restart; roll back by clearing it (falls back to DATABASE_URL / the old pooler). Trigger reads DATABASE_URL_TRIGGER from its trigger.dev env.

Type of Change

  • New feature (infra plumbing; inert until configured)

Testing

Tested manually. New packages/db unit tests for resolveDbUrl (keyed-preferred, base-fallback, undefined-when-unset, replica variant, role uppercasing). bun run lint, check:api-validation:strict, @sim/db vitest (24/24), and packages/db + @sim/realtime typechecks all pass.

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

@vercel

vercel Bot commented Jun 30, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Jun 30, 2026 2:01am

Request Review

@cursor

cursor Bot commented Jun 30, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Changes how every process picks its DB DSN at startup; misconfigured role URLs could route traffic to the wrong pooler, though unset overrides preserve current behavior.

Overview
Adds per-role Postgres connection URL resolution so web, trigger, and realtime can point at separate users/PgBouncers via env only, without changing call sites.

@sim/db introduces resolveDbUrl(base, role), which prefers DATABASE_URL_<ROLE> / DATABASE_REPLICA_URL_<ROLE> and falls back to the shared DATABASE_URL / DATABASE_REPLICA_URL. packages/db/db.ts uses it for primary and replica pools keyed off SIM_DB_ROLE. Unset role-specific vars keep today’s single-URL behavior.

Realtime sets SIM_DB_ROLE=realtime in bootstrap and npm scripts, and socketDb uses the same resolver so both realtime pools align when DATABASE_URL_REALTIME is set. Optional DATABASE_URL_* / DATABASE_REPLICA_URL_* vars are declared in the sim and realtime env schemas. Unit tests cover resolveDbUrl fallback and precedence.

Reviewed by Cursor Bugbot for commit 6cdb5a6. Bugbot is set up for automated code reviews on this repo. Configure here.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 98c5fdd. Configure here.

Comment thread apps/realtime/src/database/operations.ts
@greptile-apps

greptile-apps Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds role-based database URL resolution for each runtime surface. The main changes are:

  • Prefer DATABASE_URL_<ROLE> and DATABASE_REPLICA_URL_<ROLE> when present.
  • Fall back to the existing shared database URLs when keyed values are unset.
  • Pin the realtime process to the realtime database role during startup.
  • Declare the new optional env vars for the app and realtime services.
  • Add unit tests for the shared URL resolver.

Confidence Score: 5/5

This looks safe to merge.

  • No blocking issues found in the changed code.

Important Files Changed

Filename Overview
apps/realtime/package.json Realtime scripts now start with the realtime database role.
apps/realtime/src/bootstrap.ts Realtime bootstrap now sets the database role before loading the server.
apps/realtime/src/database/operations.ts The socket database pool now uses the shared role-based URL resolver.
apps/realtime/src/env.ts Realtime env validation now includes optional realtime database URLs.
apps/sim/lib/core/config/env.ts The app env schema now includes optional role-keyed database URLs.
packages/db/connection-url.ts A shared helper now resolves role-keyed database URLs with base fallbacks.
packages/db/db.ts Primary and replica clients now resolve URLs from the active database role.
packages/db/index.ts The database package now exports the URL resolver.
packages/db/connection-url.test.ts New tests cover keyed URL preference, fallback behavior, and role casing.

Reviews (2): Last reviewed commit: "fix(db): pin realtime process to SIM_DB_..." | Re-trigger Greptile

Comment thread apps/realtime/src/database/operations.ts
…hare the role

Without it, the realtime process left SIM_DB_ROLE unset: the shared @sim/db
client defaulted role to 'web' (web pool profile + DATABASE_URL_WEB) while
socketDb used 'realtime', so the two pools diverged after cutover. Set it at the
process level (bootstrap + dev/start scripts), mirroring DB_APP_NAME, so the
shared client and socketDb both resolve the realtime profile and URL.
@TheodoreSpeaks TheodoreSpeaks requested a review from a team as a code owner June 30, 2026 02:01
@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

Addressed the Bugbot + Greptile finding (realtime dual-pool role split) in 6cdb5a6.

Root cause (real bug): the realtime process never set SIM_DB_ROLE. So the shared @sim/db client defaulted role → web (DATABASE_URL_WEB + the web pool profile) while socketDb used realtime — the two pools would diverge after cutover. (It also meant the shared client was already silently on the web profile, max 10 instead of 5.)

Fix: pin SIM_DB_ROLE=realtime at the process level, mirroring the existing DB_APP_NAME pattern, so the role lands before @sim/db reads it:

  • apps/realtime/src/bootstrap.ts: process.env.SIM_DB_ROLE ??= 'realtime' (prod entry, before the dynamic @/index import)
  • apps/realtime/package.json dev/start: prefixed SIM_DB_ROLE=realtime (local path that bypasses bootstrap)

Now both realtime pools (shared @sim/db and socketDb) resolve the same realtime role → same DATABASE_URL_REALTIME and the same realtime pool profile. bun run lint + @sim/realtime typecheck pass.

@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@greptile review

@TheodoreSpeaks TheodoreSpeaks merged commit 0613ceb into staging Jun 30, 2026
16 checks passed
@TheodoreSpeaks TheodoreSpeaks deleted the feat/db-url-per-role branch June 30, 2026 19:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant