Phase B of ADR-0002. Makes postgres/iac, argocd, and the conventions docs multi-environment-capable without activating any sandbox. Every app stays prod-only, so this PR is behaviour-neutral — the machinery is in place but inert until Phase D flips a switch.
⚠️The correctness gate for this PR is the CI tofu plan showing zero changes on postgres/iac — not the textual diff. postgres/iac provisions production databases; the design guarantees a no-op plan, but please confirm the plan is empty before merging.
Why it's a no-op (proven, not asserted)
postgres/iac — the applications variable becomes set(object({name, envs=optional([\"prod\"])})), and a local.app_instances flatten maps applications × envs → instances keyed by the elided id. For a prod-only app the key is the bare name, database=<app>, role=<app>_role — character-for-character identical to the old set(string) for_each. I verified the flatten output with a standalone tofu apply:
erp → { database = "erp", role = "erp_role" }
webapp → { database = "webapp", role = "webapp_role" }
crowdsec → { database = "crowdsec", role = "crowdsec_role" }
plausible → { database = "plausible", role = "plausible_role" }
dance-lessons-coach → { database = "dance-lessons-coach", role = "dance-lessons-coach_role" }
So postgresql_database.app_db[\"erp\"], postgresql_role.app_role[\"erp\"] (name erp_role), etc. keep their exact Terraform addresses and attributes → no resource is dropped or recreated.
argocd — apps.yaml gains a range $app_attr.envs loop after the prod Application. Since no app declares envs, it renders nothing extra: helm template before vs after diffs to empty (verified). When an app does declare envs (Phase D), it additionally renders <app>-<env> Applications (shared repoURL, values-<env>.yaml overlay, own namespace) — I previewed erp.envs.sandbox and it produced a correct erp-sandbox Application alongside the unchanged erp one.
Files
File
Change
postgres/iac/variables.tf
applications → set(object({name, envs}))
postgres/iac/main.tf
local.app_instances elision flatten; per-app resources iterate it via each.key / each.value.{database,role}. Also a full tofu fmt pass (cosmetic — the pgbouncer function block reindents 4→2 spaces).
postgres/iac/terraform.tfvars
string entries → { name = "..." }
argocd/templates/apps.yaml
per-env Application loop (inert until an app sets envs)
EN mirror: env-coordinate section + "Two sandbox models" reconciling ADR-0001 (names repeat behind a cluster fence) vs ADR-0002 (<env> suffix in-cluster); Last Updated bumped; ADR-0002 cross-links
Note on the guidebook reconciliation
The existing naming-conventions guidebook argued names repeat across environments (and disparaged "erp-staging-style aliases"). That holds for the separate-cluster sandbox (ADR-0001). ADR-0002's in-cluster sibling has no cluster fence, so the <env> suffix is the disambiguator. I reframed that section to present both models cleanly rather than leave the contradiction.
Verification done locally
tofu validate (postgres/iac) passes; tofu fmt -check clean on my 3 tf files
flatten keys proven byte-identical to prod via standalone tofu apply
all new doc cross-links resolve; Last Updated stamps bumped
CI tofu plan no-op on postgres/iac ← the merge gate (please confirm)
Not in this PR
Phase D — activate erp-sandbox (add envs=[\"prod\",\"sandbox\"] to erp in postgres tfvars + argocd values + erp iac/main.tf for_each). Real resources appear there, gated by its own plan review.
Phase E — DNS erp-sandbox.arcodange.lab + write-scoped ai_agent_sandbox Dolibarr user.
## Summary
Phase B of [ADR-0002](https://gitea.arcodange.lab/arcodange-org/factory/src/branch/main/vibe/ADR/0002-per-application-environments.md). Makes `postgres/iac`, `argocd`, and the conventions docs **multi-environment-capable without activating any sandbox**. Every app stays prod-only, so this PR is **behaviour-neutral** — the machinery is in place but inert until Phase D flips a switch.
> ⚠️ **The correctness gate for this PR is the CI `tofu plan` showing zero changes** on `postgres/iac` — not the textual diff. `postgres/iac` provisions production databases; the design guarantees a no-op plan, but please confirm the plan is empty before merging.
### Why it's a no-op (proven, not asserted)
**postgres/iac** — the `applications` variable becomes `set(object({name, envs=optional([\"prod\"])}))`, and a `local.app_instances` flatten maps `applications × envs` → instances keyed by the elided id. For a prod-only app the key is the bare name, `database=<app>`, `role=<app>_role` — character-for-character identical to the old `set(string)` for_each. I verified the flatten output with a standalone `tofu apply`:
```
erp → { database = "erp", role = "erp_role" }
webapp → { database = "webapp", role = "webapp_role" }
crowdsec → { database = "crowdsec", role = "crowdsec_role" }
plausible → { database = "plausible", role = "plausible_role" }
dance-lessons-coach → { database = "dance-lessons-coach", role = "dance-lessons-coach_role" }
```
So `postgresql_database.app_db[\"erp\"]`, `postgresql_role.app_role[\"erp\"]` (name `erp_role`), etc. keep their exact Terraform addresses and attributes → no resource is dropped or recreated.
**argocd** — `apps.yaml` gains a `range $app_attr.envs` loop after the prod Application. Since no app declares `envs`, it renders nothing extra: `helm template` before vs after diffs to **empty** (verified). When an app *does* declare `envs` (Phase D), it additionally renders `<app>-<env>` Applications (shared repoURL, `values-<env>.yaml` overlay, own namespace) — I previewed `erp.envs.sandbox` and it produced a correct `erp-sandbox` Application alongside the unchanged `erp` one.
### Files
| File | Change |
|---|---|
| `postgres/iac/variables.tf` | `applications` → `set(object({name, envs}))` |
| `postgres/iac/main.tf` | `local.app_instances` elision flatten; per-app resources iterate it via `each.key` / `each.value.{database,role}`. Also a full `tofu fmt` pass (cosmetic — the pgbouncer function block reindents 4→2 spaces). |
| `postgres/iac/terraform.tfvars` | string entries → `{ name = "..." }` |
| `argocd/templates/apps.yaml` | per-env Application loop (inert until an app sets `envs`) |
| `doc/runbooks/new-web-app/conventions.md` | FR: new "Plusieurs environnements" section (elision rule, suffix, owner-role exception, erp/erp-sandbox table) |
| `vibe/guidebooks/lab-ecosystem/naming-conventions.md` | EN mirror: env-coordinate section + "Two sandbox models" reconciling ADR-0001 (names repeat behind a cluster fence) vs ADR-0002 (`<env>` suffix in-cluster); `Last Updated` bumped; ADR-0002 cross-links |
### Note on the guidebook reconciliation
The existing naming-conventions guidebook argued names *repeat* across environments (and disparaged "erp-staging-style aliases"). That holds for the **separate-cluster** sandbox (ADR-0001). ADR-0002's **in-cluster sibling** has no cluster fence, so the `<env>` suffix *is* the disambiguator. I reframed that section to present both models cleanly rather than leave the contradiction.
### Verification done locally
- [x] `tofu validate` (postgres/iac) passes; `tofu fmt -check` clean on my 3 tf files
- [x] flatten keys proven byte-identical to prod via standalone `tofu apply`
- [x] argocd `helm template` diff empty (prod render unchanged); Phase-D preview renders `erp-sandbox` correctly
- [x] all new doc cross-links resolve; `Last Updated` stamps bumped
- [ ] **CI `tofu plan` no-op on postgres/iac** ← the merge gate (please confirm)
### Not in this PR
- **Phase D** — activate `erp-sandbox` (add `envs=[\"prod\",\"sandbox\"]` to erp in postgres tfvars + argocd values + erp `iac/main.tf` for_each). Real resources appear there, gated by its own plan review.
- **Phase E** — DNS `erp-sandbox.arcodange.lab` + write-scoped `ai_agent_sandbox` Dolibarr user.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
ADR-0002 Phase B. Makes postgres/iac, argocd, and the conventions docs
multi-environment-capable WITHOUT activating any sandbox yet — every app
stays prod-only, so this change is behaviour-neutral:
- postgres/iac `tofu plan` is a no-op (proven: the elision flatten keys
are bare app names, db=<app>, role=<app>_role — identical addresses)
- the argocd apps.yaml render is byte-identical (181→181 lines, diff
empty) since no app declares `envs`
postgres/iac:
- variables.tf: `applications` becomes set(object({name, envs=optional(["prod"])}))
- main.tf: a `local.app_instances` flatten of applications × envs keyed by the
elided instance id (env=prod → "<app>"); per-app resources iterate it and
reference each.key / each.value.{database,role}. For prod-only apps every
resource address + attribute is unchanged. (main.tf also got a full
`tofu fmt` pass — the pgbouncer function block reindents 4→2 spaces, which
is cosmetic; the correctness gate is the CI tofu plan, not the text diff.)
- terraform.tfvars: string entries → { name = "..." } objects.
argocd/templates/apps.yaml:
- after the prod Application, a `range $app_attr.envs` loop renders one extra
Application per non-prod env: name/namespace `<app>-<env>`, shared repoURL,
helm.valueFiles [values.yaml, values-<env>.yaml], per-env syncPolicy override.
Renders nothing while no app sets `envs` → prod render unchanged.
docs:
- doc/runbooks/new-web-app/conventions.md (FR, authoritative): new section
"Plusieurs environnements pour une même app" — elision rule, suffix rule,
snake-case owner-role exception, erp/erp-sandbox table, ADR-0002 link.
- vibe/guidebooks/lab-ecosystem/naming-conventions.md (EN mirror): the env
coordinate section + a "Two sandbox models" section reconciling the
separate-cluster (ADR-0001, names repeat) vs in-cluster sibling (ADR-0002,
<env> suffix) strategies; Last Updated bumped; ADR-0002 cross-links.
Activation (erp gets envs=["prod","sandbox"] in postgres tfvars + argocd
values + erp/iac) is Phase D, gated by its own plan review.
Refs ADR-0002 (factory#15). Phase A = tools#2 (merged). Phase C = erp#11 (merged).
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
Phase B of ADR-0002. Makes
postgres/iac,argocd, and the conventions docs multi-environment-capable without activating any sandbox. Every app stays prod-only, so this PR is behaviour-neutral — the machinery is in place but inert until Phase D flips a switch.Why it's a no-op (proven, not asserted)
postgres/iac — the
applicationsvariable becomesset(object({name, envs=optional([\"prod\"])})), and alocal.app_instancesflatten mapsapplications × envs→ instances keyed by the elided id. For a prod-only app the key is the bare name,database=<app>,role=<app>_role— character-for-character identical to the oldset(string)for_each. I verified the flatten output with a standalonetofu apply:So
postgresql_database.app_db[\"erp\"],postgresql_role.app_role[\"erp\"](nameerp_role), etc. keep their exact Terraform addresses and attributes → no resource is dropped or recreated.argocd —
apps.yamlgains arange $app_attr.envsloop after the prod Application. Since no app declaresenvs, it renders nothing extra:helm templatebefore vs after diffs to empty (verified). When an app does declareenvs(Phase D), it additionally renders<app>-<env>Applications (shared repoURL,values-<env>.yamloverlay, own namespace) — I previewederp.envs.sandboxand it produced a correcterp-sandboxApplication alongside the unchangederpone.Files
postgres/iac/variables.tfapplications→set(object({name, envs}))postgres/iac/main.tflocal.app_instanceselision flatten; per-app resources iterate it viaeach.key/each.value.{database,role}. Also a fulltofu fmtpass (cosmetic — the pgbouncer function block reindents 4→2 spaces).postgres/iac/terraform.tfvars{ name = "..." }argocd/templates/apps.yamlenvs)doc/runbooks/new-web-app/conventions.mdvibe/guidebooks/lab-ecosystem/naming-conventions.md<env>suffix in-cluster);Last Updatedbumped; ADR-0002 cross-linksNote on the guidebook reconciliation
The existing naming-conventions guidebook argued names repeat across environments (and disparaged "erp-staging-style aliases"). That holds for the separate-cluster sandbox (ADR-0001). ADR-0002's in-cluster sibling has no cluster fence, so the
<env>suffix is the disambiguator. I reframed that section to present both models cleanly rather than leave the contradiction.Verification done locally
tofu validate(postgres/iac) passes;tofu fmt -checkclean on my 3 tf filestofu applyhelm templatediff empty (prod render unchanged); Phase-D preview renderserp-sandboxcorrectlyLast Updatedstamps bumpedtofu planno-op on postgres/iac ← the merge gate (please confirm)Not in this PR
erp-sandbox(addenvs=[\"prod\",\"sandbox\"]to erp in postgres tfvars + argocd values + erpiac/main.tffor_each). Real resources appear there, gated by its own plan review.erp-sandbox.arcodange.lab+ write-scopedai_agent_sandboxDolibarr user.🤖 Generated with Claude Code
ADR-0002 Phase B. Makes postgres/iac, argocd, and the conventions docs multi-environment-capable WITHOUT activating any sandbox yet — every app stays prod-only, so this change is behaviour-neutral: - postgres/iac `tofu plan` is a no-op (proven: the elision flatten keys are bare app names, db=<app>, role=<app>_role — identical addresses) - the argocd apps.yaml render is byte-identical (181→181 lines, diff empty) since no app declares `envs` postgres/iac: - variables.tf: `applications` becomes set(object({name, envs=optional(["prod"])})) - main.tf: a `local.app_instances` flatten of applications × envs keyed by the elided instance id (env=prod → "<app>"); per-app resources iterate it and reference each.key / each.value.{database,role}. For prod-only apps every resource address + attribute is unchanged. (main.tf also got a full `tofu fmt` pass — the pgbouncer function block reindents 4→2 spaces, which is cosmetic; the correctness gate is the CI tofu plan, not the text diff.) - terraform.tfvars: string entries → { name = "..." } objects. argocd/templates/apps.yaml: - after the prod Application, a `range $app_attr.envs` loop renders one extra Application per non-prod env: name/namespace `<app>-<env>`, shared repoURL, helm.valueFiles [values.yaml, values-<env>.yaml], per-env syncPolicy override. Renders nothing while no app sets `envs` → prod render unchanged. docs: - doc/runbooks/new-web-app/conventions.md (FR, authoritative): new section "Plusieurs environnements pour une même app" — elision rule, suffix rule, snake-case owner-role exception, erp/erp-sandbox table, ADR-0002 link. - vibe/guidebooks/lab-ecosystem/naming-conventions.md (EN mirror): the env coordinate section + a "Two sandbox models" section reconciling the separate-cluster (ADR-0001, names repeat) vs in-cluster sibling (ADR-0002, <env> suffix) strategies; Last Updated bumped; ADR-0002 cross-links. Activation (erp gets envs=["prod","sandbox"] in postgres tfvars + argocd values + erp/iac) is Phase D, gated by its own plan review. Refs ADR-0002 (factory#15). Phase A = tools#2 (merged). Phase C = erp#11 (merged). Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>