feat(resolve): derive DATABASE_URL/REDIS_URL from the database cast created (#60) #65

Merged
dan-claude-bot merged 1 commit from feat/derive-resource-url into main 2026-07-14 23:49:49 +00:00
dan-claude-bot commented 2026-07-14 23:47:21 +00:00 (Migrated from github.com)

Closes #60.

What

A new env-template ref, ${resource:<name>.url}, resolves to the internal URL of a database the same manifest declares — read back from the live resource's internal_db_url, never stored in the age store, never decrypted, never printed:

# core.prod.env.template
DATABASE_URL=${resource:postgres.url}
REDIS_URL=${resource:redis.url}

This deletes the two-pass generated-secret bootstrap for a database's own URL rather than automating it (cf. #48): no placeholder, no stored copy to drift or overwrite (#47), and a rotated password is simply followed on the next apply.

How

  • Grammar (envtemplate.ts): ${resource:…} parses as a distinct derived ref — capture never treats it as a store secret, templateRefs never lists it.
  • One resolver, two maps (fillDerivedEnv): at diff time against databases already on the box (so a matching app shows no drift — killing the secret DATABASE_URL differs line that ran on every plan), and in the executor at apply time against a database created earlier in the same run (from-nothing; apply acts databases-before-applications, #45).
  • The unresolved sentinel is never written. The executor refuses rather than write a blank that boots the app pointed at nothing; re-running once the database is up resolves it as an ordinary update.
  • Plan-time validation (assertResourceRefs): a ${resource:X.url} naming an undeclared database, or an attr other than .url, is a hard error in every verb that opens a template (apply, diff, capture) — mirroring the dead-generated_secrets check.
  • Diff rendering: a derived var renders DATABASE_URL: derived from database postgres …, never the value.
  • generated_secrets and the two-pass bootstrap remain for the residual non-derivable class.

The one unverified assumption

internal_db_url is a Coolify model accessor built from credentials minted at create time (not from a running container), so the URL is expected the moment the create returns — making a from-nothing apply single-pass. If a given Coolify only publishes it once the container is up, the executor refuses with a re-run instruction rather than writing a blank, and the second apply resolves it as an update. Graceful either way; single-pass in the expected case. I could not verify against a live instance.

Docs

docs/semantics.md gains a Derived resource URLs section; the README's capture walkthrough gains a pointer.

Downstream (separate, in the product repo)

The heavy-duty/incubator migration is a manifest edit, not part of this PR: switch the two templates to ${resource:…}, drop DATABASE_URL_*/REDIS_URL_* from generated_secrets, and drop the two keys from the store.

npm run build / npm test (477 passing) / npm run check all green.

🤖 Generated with Claude Code

Closes #60. ## What A new env-template ref, `${resource:<name>.url}`, resolves to the **internal URL** of a database the same manifest declares — read back from the live resource's `internal_db_url`, **never** stored in the age store, **never** decrypted, **never** printed: ``` # core.prod.env.template DATABASE_URL=${resource:postgres.url} REDIS_URL=${resource:redis.url} ``` This **deletes** the two-pass generated-secret bootstrap for a database's own URL rather than automating it (cf. #48): no placeholder, no stored copy to drift or overwrite (#47), and a rotated password is simply **followed** on the next apply. ## How - **Grammar** (`envtemplate.ts`): `${resource:…}` parses as a distinct *derived* ref — `capture` never treats it as a store secret, `templateRefs` never lists it. - **One resolver, two maps** (`fillDerivedEnv`): at **diff time** against databases already on the box (so a matching app shows no drift — killing the `secret DATABASE_URL differs` line that ran on every plan), and in the **executor at apply time** against a database created earlier in the same run (from-nothing; apply acts databases-before-applications, #45). - **The unresolved sentinel is never written.** The executor refuses rather than write a blank that boots the app pointed at nothing; re-running once the database is up resolves it as an ordinary update. - **Plan-time validation** (`assertResourceRefs`): a `${resource:X.url}` naming an undeclared database, or an attr other than `.url`, is a hard error in every verb that opens a template (apply, diff, capture) — mirroring the dead-`generated_secrets` check. - **Diff rendering**: a derived var renders `DATABASE_URL: derived from database postgres …`, never the value. - `generated_secrets` and the two-pass bootstrap **remain** for the residual non-derivable class. ## The one unverified assumption `internal_db_url` is a Coolify model accessor built from credentials minted **at create time** (not from a running container), so the URL is expected the moment the create returns — making a from-nothing apply single-pass. If a given Coolify only publishes it once the container is up, the executor **refuses with a re-run instruction** rather than writing a blank, and the second apply resolves it as an update. Graceful either way; single-pass in the expected case. I could not verify against a live instance. ## Docs `docs/semantics.md` gains a *Derived resource URLs* section; the README's capture walkthrough gains a pointer. ## Downstream (separate, in the product repo) The `heavy-duty/incubator` migration is a manifest edit, not part of this PR: switch the two templates to `${resource:…}`, drop `DATABASE_URL_*`/`REDIS_URL_*` from `generated_secrets`, and drop the two keys from the store. `npm run build` / `npm test` (477 passing) / `npm run check` all green. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: heavy-duty/cast#65
No description provided.