Kuickr

Runtime-wired DATABASE_URL — Conductor's dedicated-DB injection

Status: shipped (spec 26, Decision B). Architecture note.

The idea in one line

For a dedicated-DB app, Conductor derives DATABASE_URL from the app's provisioned dedicated container at deploy time — resolved on the box being deployed to — and never bakes it into the image, config, or repo. An explicit operator DATABASE_URL always wins.

Why it matters — the portability payoff

The app carries no database address of its own. Its connection is resolved at deploy from <slug>-db on whatever box it lands on. That's what makes an app a portable unit:

  • Move the app to another box → at deploy, DATABASE_URL automatically points at the dedicated DB on that box (same container name, different machine). Zero manual DATABASE_URL juggling. This is precisely what makes app-transfer's cut-over clean (spec 26 / plan 04).
  • Secret-safe by construction — the derived URL carries the password, so it's declared as a Kamal env.secret and injected from .kamal/secrets; it's never in the image or the repo.
  • No drift — one source of truth (the provisioned container), not a hand-maintained connection string that rots.

How it works (the code)

  • App#deploy_env_pairs(server:) — the single source of what Conductor injects (.kamal/secrets, preflight, generated config). For a dedicated app it appends ["DATABASE_URL", derived_database_url(server:)].
  • App#derived_database_url(server:) — the resolver:
    • nil unless dedicated_db?
    • nil if the operator set a DATABASE_URL env var (explicit always wins)
    • else dedicated_database(server:).database_urlpostgres://<user>:<pw>@<slug>-db:<port>/<name>, reached by container DNS on the app's docker network.
  • App#deploy_secret_keys(server:) — declares DATABASE_URL as a Kamal secret when derived, so it's actually injected (without this, a written value is never injected and preflight misfires).
  • Server-scoped server: — the box being deployed to. During a transfer the app has a dedicated DB on both source and target (same container name); each deploy resolves its own box's DSN. The same app, two boxes, two DSNs — automatically.

The override (escape hatch)

Set DATABASE_URL as an app env var and it always wins — for an external/managed DB, a read-replica DSN, or anything non-standard. derived_database_url returns nil and the derived path steps aside. No special mode; just presence of the var.

How it relates

  • Enables portable apps → app-transfer (spec 26 / plan 04): the target deploy derives the target box's DSN with no manual step.
  • The shared→dedicated converter (SharedToDedicatedConverter) removes the app's manual DATABASE_URL env var on purpose — precisely so this derived path takes over once the app is dedicated.
  • Contrast: a shared-cluster app has a hand-set DATABASE_URL env var pointing at the shared Postgres — no derivation, and (today) not transferable until converted.

Gotchas worth knowing

  • If a dedicated app also has a manual DATABASE_URL env var, the manual one wins — so a half-converted app can silently keep talking to the old DB. The converter deletes the manual var to avoid exactly this.
  • The derived URL is per-server; reading it without a server: uses the app's current server. During a transfer, always pass the box you mean.