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) <noreply@anthropic.com>
11 KiB
vibe > Guidebooks > Lab ecosystem > Naming conventions (the <app> join key)
Naming conventions — the <app> join key
Status: 🟢 Active Last Updated: 2026-06-25 Related: Lab ecosystem · Factory brick · Secrets & Vault · PRD — isolation boundary · ADR 0002 — per-application environments Upstream (source of truth): doc/runbooks/new-web-app/conventions.md (French, authoritative)
TL;DR
Every application on the platform is pinned to one kebab-case identifier — <app> (e.g. erp, webapp, url-shortener, dance-lessons-coach). That single string is reused verbatim, with no transformation, as the name of the app's Gitea repo, its PostgreSQL database and role, its Vault roles and policies, its Kubernetes namespace and ServiceAccount, its ArgoCD Application, its OpenTofu state prefix, and its DNS records. The bricks of the stack do not point at each other through explicit configuration; they wire together by guessing each other's names from <app>. Pick the name once, get it right, and the whole chain self-assembles. One typo anywhere, and the chain breaks silently.
What <app> is
<app> is a lowercase, kebab-case slug. It is the join key of the entire platform — the one value that lets a dozen otherwise-independent systems agree on which resources belong to the same application without ever exchanging a config pointer. The canonical, authoritative definition (in French) lives in the runbook: doc/runbooks/new-web-app/conventions.md. This page is the English concept summary inside the ecosystem guidebook.
The mapping — one name, every system
The table below shows how each system derives its identifier from <app>, with the erp application as the worked example.
| System | Identifier derived from <app> |
Example (erp) |
|---|---|---|
| Gitea repository | arcodange-org/<app> |
arcodange-org/erp |
| PostgreSQL database | <app> |
erp |
| PostgreSQL owner role (non-login) | <app>_role |
erp_role |
| Vault dynamic DB role | postgres/creds/<app> |
postgres/creds/erp |
| Vault Kubernetes auth role | <app> |
erp |
| Vault runtime policy (pod) | <app> |
erp |
| Vault CI/ops policy | <app>-ops |
erp-ops |
| Vault CI JWT role (Gitea OIDC) | gitea_cicd_<app> |
gitea_cicd_erp |
| Vault KV config path | kvv2/<app>/config |
kvv2/erp/config |
| Kubernetes namespace | <app> |
erp |
| Kubernetes ServiceAccount | <app> |
erp |
| ArgoCD Application | <app> |
erp |
| OpenTofu state prefix (GCS) | <app>/main |
erp/main |
| Internal DNS | <app>.arcodange.lab |
erp.arcodange.lab |
| Public DNS | <app>.arcodange.fr |
erp.arcodange.fr |
Note
The
_rolesuffix (PG owner role) and the-opssuffix (Vault CI policy/identity group) are the only two systematic transformations of<app>. Everything else uses the bare slug. Note the suffix style differs: PostgreSQL uses an underscore (erp_role) because hyphens are awkward in SQL identifiers, whereas Vault and Kubernetes use a hyphen (erp-ops).
Why uniformity is structuring
The platform is a set of loosely-coupled bricks (Gitea, Postgres, Vault, k3s/ArgoCD, OpenTofu, DNS). They were deliberately built not to hold explicit references to one another. Instead, each brick reconstructs the names it needs from <app> at the moment it runs:
%%{init: {'theme':'base'}}%%
flowchart LR
APP["<app><br/>(one kebab-case slug)"]:::src
APP --> GIT["Gitea repo<br/>arcodange-org/<app>"]:::brick
APP --> PG["PostgreSQL<br/>db <app> · role <app>_role"]:::brick
APP --> VAULT["Vault<br/>postgres/creds/<app><br/>policy <app> · gitea_cicd_<app>"]:::brick
APP --> K8S["Kubernetes<br/>namespace + SA <app>"]:::brick
APP --> ARGO["ArgoCD<br/>Application <app>"]:::brick
APP --> GCS["OpenTofu state<br/><app>/main"]:::brick
APP --> DNS["DNS<br/><app>.arcodange.lab / .fr"]:::brick
VAULT -.->|"GRANT <app>_role<br/>assumes PG role name"| PG
K8S -.->|"VaultDynamicSecret reads<br/>postgres/creds/<app>"| VAULT
ARGO -.->|"repoURL=.../<app><br/>namespace=<app>"| GIT
classDef src fill:#2563eb,stroke:#1e40af,color:#fff
classDef brick fill:#059669,stroke:#047857,color:#fff
- The chosen slug
<app>is the single input. - From it, each brick names its own resource: Gitea names the repo
arcodange-org/<app>; Postgres names the database<app>and its owner role<app>_role; Vault names the dynamic-creds rolepostgres/creds/<app>, the runtime policy<app>, and the CI JWT rolegitea_cicd_<app>; Kubernetes names the namespace and ServiceAccount<app>; ArgoCD names the Application<app>; OpenTofu writes state under<app>/main; DNS publishes<app>.arcodange.laband<app>.arcodange.fr. - The dashed arrows are the cross-brick assumptions that make it work: the Vault
app_rolesmodule issues a dynamic PG user withGRANT <app>_role TO …, assuming the Postgres owner role is named exactly<app>_role; the chart'sVaultDynamicSecretreadspostgres/creds/<app>, assuming the Vault role is named exactly<app>; the ArgoCD Application derivesrepoURL=.../<app>andnamespace=<app>from the slug alone, assuming the Gitea repo and the namespace match. - None of these links is configured by hand. They hold purely because every brick was given the same
<app>to reconstruct from.
The failure mode of a typo
Because the wiring is by name and not by explicit reference, nothing validates the join key end-to-end. A single divergence — my_app vs my-app, a stray capital (MyApp), an accidental plural (erps) — does not raise an error at creation time. The mismatched brick simply builds a resource under a name no one else looks for:
- A Postgres owner role created as
erp-role(hyphen) instead oferp_role→ Vault'sGRANT erp_rolefails or grants nothing → the pod gets a DB user with no privileges. - A Gitea repo named
erp-appinstead oferp→ ArgoCD's derivedrepoURL=.../erp404s → the Application never syncs. - A namespace typo → the
VaultDynamicSecretand ServiceAccount land in the wrong place → silent auth failure at pod start.
The symptom is always the same: a brick that looks provisioned but never connects, with no single component to blame. This is why the slug must be short, stable, and correct from the first step — there is no safety net downstream.
✅ Choose a short, stable, lowercase kebab-case name up front and reuse it character-for-character. ❌ Never introduce variants (case, separators, plurals); nothing will warn you.
Multiple environments per app (the <env> coordinate)
A single application can run as several deployed instances — prod, sandbox, and so on — without becoming a separate app: same repo, same chart, same version. A second coordinate <env> extends the join key, governed by an elision rule (ADR 0002):
envdefaults toprod, andprodelides — whenenv == prodno suffix is added, so every derived name is exactly the single-coordinate output of the mapping above. Existing apps are unaffected (their plan is a no-op).- Non-prod envs take the
<app>-<env>suffix everywhere — namespace, Vault paths / roles / policies, ArgoCD Application, DNS, GCS state sub-prefix — with the one snake-case exception inherited from the_roleconvention: the Postgres owner role is<app>_<env>_role. - One repo, one chart, and one CI JWT role (
gitea_cicd_<app>) serve every env; per-env differences are avalues-<env>.yamloverlay.
Worked example — erp (prod, elided) and erp-sandbox:
| System | erp (env = prod) |
erp-sandbox |
|---|---|---|
| PostgreSQL database | erp |
erp-sandbox |
| PostgreSQL owner role | erp_role |
erp_sandbox_role |
| Namespace + ServiceAccount | erp |
erp-sandbox |
| Vault dynamic DB creds | postgres/creds/erp |
postgres/creds/erp-sandbox |
| Vault KV config | kvv2/erp/config |
kvv2/erp-sandbox/config |
| ArgoCD Application | erp |
erp-sandbox |
| Internal DNS | erp.arcodange.lab |
erp-sandbox.arcodange.lab |
| Gitea repo / chart / CI JWT | arcodange-org/erp · chart · gitea_cicd_erp |
shared |
Two sandbox models, two naming strategies
There are two distinct ways to stand up a non-production copy, and they treat the join key differently — by design, not by accident.
- Separate-cluster sandbox (ADR 0001) — a whole parallel universe (its own Vault, Postgres, k3s) on the control node, for rehearsing dangerous infrastructure changes. The two universes never share a backing store, so identity comes from the environment boundary, not the name: the sandbox hosts an
erpnamed identically to production. Names repeat freely; no<env>suffix is needed, so the name-wiring stays intact and drills run against realistic identifiers. - In-cluster sibling instance (ADR 0002) — a second instance on the same cluster (e.g.
erp-sandboxbesideerp), for rehearsing application-data writes against the real API. Here there is no cluster fence to disambiguate by, so the<env>suffix is the separator: every derived name carries-sandboxto avoid colliding with prod's namespace, database, Vault paths, and DNS.
Both keep the name-wiring coherent — one by repeating the slug behind a cluster fence, the other by extending the slug with the elided <env> coordinate. See the PRD's isolation boundary for how the separate-cluster fence is drawn, and ADR 0002 for why the in-cluster sibling's blast radius stays bounded to one app's data.
See also
- doc/runbooks/new-web-app/conventions.md — the authoritative French source, with per-step references into the 8-step "new web app" runbook.
- Secrets & Vault — how
gitea_cicd_<app>and the<app>/<app>-opspolicies fit the auth model. - Factory brick — where the ArgoCD app-of-apps, the Postgres OpenTofu, and the IaC live.
- PRD — isolation boundary — why identical names are safe across environments.
- ADR 0001 — Safe, production-like environment — the separate-cluster sandbox model.
- ADR 0002 — Per-application environments — the
<env>coordinate + elision rule, and the in-cluster sibling sandbox model.