[vibe](../../README.md) > [Guidebooks](../README.md) > [Lab ecosystem](README.md) > **Naming conventions (the `` join key)** # Naming conventions โ€” the `` join key > **Status**: ๐ŸŸข Active > **Last Updated**: 2026-06-25 > **Related**: [Lab ecosystem](README.md) ยท [Factory brick](01-factory.md) ยท [Secrets & Vault](secrets-and-vault.md) ยท [PRD โ€” isolation boundary](../../PRD/safe-prod-like-environment/isolation-boundary.md) ยท [ADR 0002 โ€” per-application environments](../../ADR/0002-per-application-environments.md) > **Upstream (source of truth)**: [doc/runbooks/new-web-app/conventions.md](../../../doc/runbooks/new-web-app/conventions.md) (French, authoritative) ## TL;DR Every application on the platform is pinned to **one** kebab-case identifier โ€” `` (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 ``**. Pick the name once, get it right, and the whole chain self-assembles. One typo anywhere, and the chain breaks silently. ## What `` is `` 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](../../../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 ``, with the `erp` application as the worked example. | System | Identifier derived from `` | Example (`erp`) | | --- | --- | --- | | Gitea repository | `arcodange-org/` | `arcodange-org/erp` | | PostgreSQL database | `` | `erp` | | PostgreSQL owner role (non-login) | `_role` | `erp_role` | | Vault dynamic DB role | `postgres/creds/` | `postgres/creds/erp` | | Vault Kubernetes auth role | `` | `erp` | | Vault runtime policy (pod) | `` | `erp` | | Vault CI/ops policy | `-ops` | `erp-ops` | | Vault CI JWT role (Gitea OIDC) | `gitea_cicd_` | `gitea_cicd_erp` | | Vault KV config path | `kvv2//config` | `kvv2/erp/config` | | Kubernetes namespace | `` | `erp` | | Kubernetes ServiceAccount | `` | `erp` | | ArgoCD Application | `` | `erp` | | OpenTofu state prefix (GCS) | `/main` | `erp/main` | | Internal DNS | `.arcodange.lab` | `erp.arcodange.lab` | | Public DNS | `.arcodange.fr` | `erp.arcodange.fr` | > [!NOTE] > The `_role` suffix (PG owner role) and the `-ops` suffix (Vault CI policy/identity group) are the only two *systematic* transformations of ``. 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 `` at the moment it runs: ```mermaid %%{init: {'theme':'base'}}%% flowchart LR APP["<app>
(one kebab-case slug)"]:::src APP --> GIT["Gitea repo
arcodange-org/<app>"]:::brick APP --> PG["PostgreSQL
db <app> ยท role <app>_role"]:::brick APP --> VAULT["Vault
postgres/creds/<app>
policy <app> ยท gitea_cicd_<app>"]:::brick APP --> K8S["Kubernetes
namespace + SA <app>"]:::brick APP --> ARGO["ArgoCD
Application <app>"]:::brick APP --> GCS["OpenTofu state
<app>/main"]:::brick APP --> DNS["DNS
<app>.arcodange.lab / .fr"]:::brick VAULT -.->|"GRANT <app>_role
assumes PG role name"| PG K8S -.->|"VaultDynamicSecret reads
postgres/creds/<app>"| VAULT ARGO -.->|"repoURL=.../<app>
namespace=<app>"| GIT classDef src fill:#2563eb,stroke:#1e40af,color:#fff classDef brick fill:#059669,stroke:#047857,color:#fff ``` 1. The chosen slug `` is the single input. 2. From it, each brick names its own resource: Gitea names the repo `arcodange-org/`; Postgres names the database `` and its owner role `_role`; Vault names the dynamic-creds role `postgres/creds/`, the runtime policy ``, and the CI JWT role `gitea_cicd_`; Kubernetes names the namespace and ServiceAccount ``; ArgoCD names the Application ``; OpenTofu writes state under `/main`; DNS publishes `.arcodange.lab` and `.arcodange.fr`. 3. The dashed arrows are the cross-brick assumptions that make it work: the Vault `app_roles` module issues a dynamic PG user with `GRANT _role TO โ€ฆ`, **assuming** the Postgres owner role is named exactly `_role`; the chart's `VaultDynamicSecret` reads `postgres/creds/`, **assuming** the Vault role is named exactly ``; the ArgoCD Application derives `repoURL=.../` and `namespace=` from the slug alone, **assuming** the Gitea repo and the namespace match. 4. None of these links is configured by hand. They hold purely because every brick was given the same `` 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 of `erp_role` โ†’ Vault's `GRANT erp_role` fails or grants nothing โ†’ the pod gets a DB user with no privileges. - A Gitea repo named `erp-app` instead of `erp` โ†’ ArgoCD's derived `repoURL=.../erp` 404s โ†’ the Application never syncs. - A namespace typo โ†’ the `VaultDynamicSecret` and 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 `` 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 `` extends the join key, governed by an **elision rule** ([ADR 0002](../../ADR/0002-per-application-environments.md)): - `env` defaults to `prod`, and **`prod` elides** โ€” when `env == prod` no 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 **`-`** suffix everywhere โ€” namespace, Vault paths / roles / policies, ArgoCD Application, DNS, GCS state sub-prefix โ€” with the one snake-case exception inherited from the `_role` convention: the Postgres owner role is `__role`. - One repo, one chart, and one CI JWT role (`gitea_cicd_`) serve every env; per-env differences are a `values-.yaml` overlay. 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](../../ADR/0001-safe-prod-like-environment.md)) โ€” 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 `erp` named identically to production. Names repeat freely; no `` suffix is needed, so the name-wiring stays intact and drills run against realistic identifiers. - **In-cluster sibling instance** ([ADR 0002](../../ADR/0002-per-application-environments.md)) โ€” a second instance on the *same* cluster (e.g. `erp-sandbox` beside `erp`), for rehearsing *application-data* writes against the real API. Here there is no cluster fence to disambiguate by, so the `` suffix *is* the separator: every derived name carries `-sandbox` to 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 `` coordinate. See the PRD's [isolation boundary](../../PRD/safe-prod-like-environment/isolation-boundary.md) for how the separate-cluster fence is drawn, and [ADR 0002](../../ADR/0002-per-application-environments.md) 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](../../../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](secrets-and-vault.md) โ€” how `gitea_cicd_` and the `` / `-ops` policies fit the auth model. - [Factory brick](01-factory.md) โ€” where the ArgoCD app-of-apps, the Postgres OpenTofu, and the IaC live. - [PRD โ€” isolation boundary](../../PRD/safe-prod-like-environment/isolation-boundary.md) โ€” why identical names are safe across environments. - [ADR 0001 โ€” Safe, production-like environment](../../ADR/0001-safe-prod-like-environment.md) โ€” the separate-cluster sandbox model. - [ADR 0002 โ€” Per-application environments](../../ADR/0002-per-application-environments.md) โ€” the `` coordinate + elision rule, and the in-cluster sibling sandbox model.