diff --git a/README.md b/README.md
index df520da..9207c23 100644
--- a/README.md
+++ b/README.md
@@ -80,4 +80,9 @@ classDef done fill:gold,stroke:indigo,stroke-width:4px,color:blue;
class prepare_hd,nodeId2 done;
```
+## Documentation
+
+- đ [`doc/`](doc/README.md) â ADR (dĂ©cisions d'architecture) + runbooks.
+- đ [Runbook : mettre en service une nouvelle application web](doc/runbooks/new-web-app/README.md) â dĂ©pĂŽt Gitea, base de donnĂ©es, Vault, chart Helm, Terraform, CI, ArgoCD.
+
đčđ»đȘœ
diff --git a/doc/README.md b/doc/README.md
new file mode 100644
index 0000000..f9262dd
--- /dev/null
+++ b/doc/README.md
@@ -0,0 +1,35 @@
+[Factory](../README.md) > **Doc**
+
+# Documentation Factory
+
+> **Last Updated:** 2026-05-31
+> **Status:** Production · maintenue activement
+> **Related:** [README racine du dépÎt](../README.md) · [Collection Ansible `arcodange.factory`](../ansible/arcodange/factory/README.md)
+
+## C'est quoi ?
+
+Le dossier `doc/` rassemble la documentation de la plateforme Arcodange (k3s + Gitea + ArgoCD + OpenTofu + Vault + Postgres, auto-hébergée sur 3 Raspberry Pi). On y trouve deux familles :
+
+- les **ADR** (records de décisions d'architecture), qui expliquent *pourquoi* la plateforme est faite ainsi ;
+- les **runbooks**, qui expliquent *comment* exécuter une procédure opérationnelle de bout en bout.
+
+> [!NOTE]
+> Convention du dĂ©pĂŽt (cf. PR #8) : la documentation vit sous `doc/` (**singulier**). Un ancien dossier `docs/` (pluriel) a pu traĂźner non-suivi par git â ne pas y ajouter de contenu.
+
+## Sections
+
+| Section | Ce qu'on y trouve | Statut |
+|---|---|---|
+| [Runbooks](runbooks/README.md) | ProcĂ©dures opĂ©rationnelles pas-Ă -pas (crĂ©er une app, etc.) | â
|
+| [ADR](adr/README.md) | DĂ©cisions d'architecture + checklist de mise en place de la plateforme | â
|
+
+## Légende de statut
+
+â
actif · đĄ dĂ©gradĂ©/beta · đŽ critique/EOL · â ïž problĂšme connu · â dĂ©sactivĂ©
+
+## Comment éditer cette documentation
+
+1. **Ajouter une page** â la crĂ©er depuis le template adĂ©quat **et** ajouter sa ligne dans la table d'index du `README.md` parent.
+2. **Supprimer une page** â la marquer *DĂ©commissionnĂ©e (date)* d'abord ; supprimer le fichier et sa ligne d'index ensemble une fois qu'elle est vraiment partie.
+3. **Garder les liens croisĂ©s bidirectionnels** â quand on lie AâB, ajouter BâA.
+4. **Mettre Ă jour `Last Updated:`** en tĂȘte de la racine concernĂ©e aprĂšs tout changement de structure.
diff --git a/doc/adr/README.md b/doc/adr/README.md
index 1166776..5fdfe7f 100644
--- a/doc/adr/README.md
+++ b/doc/adr/README.md
@@ -18,9 +18,9 @@
- [ ] terrakube
- [ ] prometheus/grafana
- [ ] ansible AWX
-- [ ] setup hello world web app
- - [ ] manage postgres credentials
- - [ ] protect public endpoint (crowdsec)
+- [ ] setup hello world web app â đ procĂ©dure complĂšte : [runbook « Nouvelle application web »](../runbooks/new-web-app/README.md)
+ - [ ] manage postgres credentials â [base de donnĂ©es](../runbooks/new-web-app/02-database.md) + [Vault plateforme](../runbooks/new-web-app/03-vault-platform.md)
+ - [ ] protect public endpoint (crowdsec) â [chart : ingress public](../runbooks/new-web-app/04-helm-chart.md)
> [!NOTE]
> Reference: [Arcodange _**Factory**_ Ansible Collection](/ansible/arcodange/factory/README.md)
diff --git a/doc/runbooks/README.md b/doc/runbooks/README.md
new file mode 100644
index 0000000..75ce0ca
--- /dev/null
+++ b/doc/runbooks/README.md
@@ -0,0 +1,21 @@
+[Factory](../../README.md) > [Doc](../README.md) > **Runbooks**
+
+# Runbooks
+
+> **Scope :** procédures opérationnelles pas-à -pas de la plateforme Arcodange. Chaque runbook se lit du début à la fin et mÚne à un résultat vérifiable. Pour le *pourquoi* (décisions d'architecture), voir les [ADR](../adr/README.md).
+
+```mermaid
+%%{init: {'theme': 'base'}}%%
+flowchart LR
+ classDef node fill:#059669,stroke:#047857,color:#fff
+ OP["Opérateur"]:::node --> RB["Runbook
(procédure ordonnée)"]:::node --> RES["Résultat vérifiable
(app en ligne, etc.)"]:::node
+```
+
+## Index
+
+| Runbook | Résumé | Statut |
+|---|---|---|
+| [Nouvelle application web](new-web-app/README.md) | CrĂ©er une app web de zĂ©ro dans un nouveau dĂ©pĂŽt Gitea : dĂ©pĂŽt, base de donnĂ©es, Vault, chart Helm, Terraform, CI, ArgoCD | â
|
+
+> [!TIP]
+> Pour ajouter un runbook : créer un dossier `kebab-case/` avec son `README.md` (front door : intro + diagramme + index), puis ajouter sa ligne ci-dessus.
diff --git a/doc/runbooks/new-web-app/01-gitea-repo.md b/doc/runbooks/new-web-app/01-gitea-repo.md
new file mode 100644
index 0000000..9009c81
--- /dev/null
+++ b/doc/runbooks/new-web-app/01-gitea-repo.md
@@ -0,0 +1,88 @@
+[Factory](../../../README.md) > [Doc](../../README.md) > [Runbooks](../README.md) > [Nouvelle application web](README.md) > **1. DépÎt Gitea**
+
+# 1. Créer le dépÎt Gitea
+
+> **Status:** â
Active
+> **Downstream:** [2. Base de données](02-database.md), [3. Vault plateforme](03-vault-platform.md)
+> **Related:** [Conventions de nommage](conventions.md) · [6. Workflows CI](06-ci-workflows.md)
+
+---
+
+## Summary
+
+Tout part d'un dépÎt Gitea nommé exactement `` (voir [conventions](conventions.md)). Le créer **sous l'organisation `arcodange-org`** n'est pas un détail : c'est ce qui lui fait **hériter automatiquement des secrets Actions d'organisation** dont tous les workflows dépendent. Le dépÎt contient un squelette fixe que les étapes suivantes viennent remplir.
+
+## Pourquoi sous `arcodange-org`
+
+Les workflows `.gitea/workflows/*` (voir [étape 6](06-ci-workflows.md)) référencent des secrets qui ne sont **pas** définis dans le dépÎt mais au niveau de l'organisation et hérités par tous ses dépÎts :
+
+| Secret d'organisation | Usage |
+|---|---|
+| `HOMELAB_CA_CERT` | CA interne (base64) pour parler en TLS Ă `vault.arcodange.lab` |
+| `vault_oauth__sh_b64` | Script (base64) qui rĂ©alise l'Ă©change OIDC Gitea â JWT Vault |
+| `PACKAGES_TOKEN` | Token de push vers le registre d'images `gitea.arcodange.lab` |
+
+Ces secrets sont propagés par le rÎle Ansible [`gitea_secret`](https://gitea.arcodange.lab/arcodange-org/factory/src/branch/main/ansible/arcodange/factory/roles/gitea_secret/defaults/main.yml) (`gitea_owner_type: organization`).
+
+> [!IMPORTANT]
+> Un dĂ©pĂŽt créé **hors** `arcodange-org` (par ex. sous l'org `arcodange`) n'hĂ©rite pas forcĂ©ment de ces secrets. Si tu surcharges l'org (cf. `org:` Ă l'[Ă©tape 7](07-argocd-register.md)), assure-toi que les mĂȘmes secrets y existent.
+
+## Options pour créer le dépÎt
+
+| Méthode | Quand | Comment |
+|---|---|---|
+| UI Gitea | one-shot manuel | `https://gitea.arcodange.lab` â New Repository sous `arcodange-org` |
+| MCP Gitea | depuis un agent | outil `mcp__gitea__create_repo` (cf. rÚgle « Gitea = MCP, pas `gh` » du guide global) |
+| RĂŽle Ansible `gitea_repo` | reproductible/inventaire | [`roles/gitea_repo`](https://gitea.arcodange.lab/arcodange-org/factory/src/branch/main/ansible/arcodange/factory/roles/gitea_repo/defaults/main.yml) |
+| Ressource Terraform `gitea_repository` | tout-en-IaC | dans [`factory/iac`](https://gitea.arcodange.lab/arcodange-org/factory/src/branch/main/iac) (provider `go-gitea/gitea` déjà configuré) |
+
+## Squelette minimal du dépÎt
+
+```
+/
+âââ chart/ # chart Helm â ArgoCD dĂ©ploie CE dossier (path: chart) â Ă©tape 4
+â âââ Chart.yaml
+â âââ values.yaml
+â âââ templates/
+âââ iac/ # Terraform/OpenTofu de l'app â Ă©tape 5
+â âââ providers.tf
+â âââ backend.tf
+â âââ main.tf
+âââ .gitea/workflows/ # CI (tofu apply + build image) â Ă©tape 6
+â âââ vault.yaml
+â âââ dockerimage.yaml # uniquement si l'app build sa propre image
+âââ Dockerfile # uniquement si l'app build sa propre image
+âââ README.md
+âââ .gitignore
+```
+
+> [!IMPORTANT]
+> Le dossier du chart **doit** s'appeler `chart/` et ĂȘtre Ă la racine : le template ArgoCD impose `path: chart` (cf. [Ă©tape 7](07-argocd-register.md)). Pas de `helm/`, pas de sous-dossier.
+
+## `.gitignore` recommandé
+
+Aligné sur les dépÎts existants (exclut tout secret local) :
+
+```gitignore
+.terraform
+.terraform.*
+.env
+*.key
+secrets/
+.DS_Store
+```
+
+## Le bot `tofu_module_reader`
+
+La CI de l'app clone le module Terraform partagé `tools` **en SSH** (cf. [étape 5](05-app-terraform.md)). C'est l'utilisateur restreint `tofu_module_reader` (créé dans [`factory/iac/gitea_tofu_ci_user.tf`](https://gitea.arcodange.lab/arcodange-org/factory/src/branch/main/iac/gitea_tofu_ci_user.tf), clé privée dans Vault `kvv1/gitea/tofu_module_reader`) qui sert d'identité de lecture. Rien à faire de spécial, mais le dépÎt `tools` doit rester lisible par ce bot.
+
+## Notes / contraintes
+
+- Le nom du dĂ©pĂŽt = `` **exactement** (kebab-case minuscule). Voir [conventions](conventions.md) â ce nom se propage partout.
+- Pas besoin de protéger `main` autrement que par la convention worktree/PR de l'équipe ; ArgoCD suit `HEAD`.
+
+## Related
+
+- [2. Base de donnĂ©es](02-database.md) â provisionner la base, en parallĂšle de l'Ă©tape 3.
+- [3. Vault plateforme](03-vault-platform.md) â dĂ©clarer l'app cĂŽtĂ© Vault, en parallĂšle de l'Ă©tape 2.
+- [6. Workflows CI](06-ci-workflows.md) â consomme les secrets d'org hĂ©ritĂ©s ici.
diff --git a/doc/runbooks/new-web-app/02-database.md b/doc/runbooks/new-web-app/02-database.md
new file mode 100644
index 0000000..b6dc0a6
--- /dev/null
+++ b/doc/runbooks/new-web-app/02-database.md
@@ -0,0 +1,93 @@
+[Factory](../../../README.md) > [Doc](../../README.md) > [Runbooks](../README.md) > [Nouvelle application web](README.md) > **2. Base de données**
+
+# 2. Provisionner la base de données
+
+> **Status:** â
Active
+> **Upstream:** [1. DépÎt Gitea](01-gitea-repo.md)
+> **Downstream:** [5. Terraform de l'app](05-app-terraform.md)
+> **Related:** [3. Vault plateforme](03-vault-platform.md) · [4. Chart Helm](04-helm-chart.md) · [Conventions de nommage](conventions.md)
+
+---
+
+## Summary
+
+La base de l'app et son **rÎle propriétaire** `_role` sont créés par un Terraform **cÎté plateforme** (`factory/postgres/iac`), pas dans le dépÎt de l'app. On ajoute simplement le nom de l'app à une liste, et la CI de `factory` applique. L'app, elle, ne se connectera **jamais** avec un mot de passe statique : elle obtiendra des identifiants éphémÚres de Vault (cf. [étape 4](04-helm-chart.md)) qui héritent de `_role`.
+
+## Action
+
+Ajouter `""` au set `applications` de [`factory/postgres/iac/terraform.tfvars`](https://gitea.arcodange.lab/arcodange-org/factory/src/branch/main/postgres/iac/terraform.tfvars) :
+
+```hcl
+applications = [
+ "webapp",
+ "erp",
+ "crowdsec",
+ "plausible",
+ "dance-lessons-coach",
+ "", # â ajouter
+]
+```
+
+Puis pousser : la CI applique automatiquement (voir plus bas).
+
+## Ce que ça crée
+
+[`postgres/iac/main.tf`](https://gitea.arcodange.lab/arcodange-org/factory/src/branch/main/postgres/iac/main.tf) itÚre `for_each` sur le set et crée, **par app** :
+
+| Ressource | Nom | RĂŽle |
+|---|---|---|
+| `postgresql_role` | `_role` | RÎle **non-login**, propriétaire de la base |
+| `postgresql_grant_role` | `_role` â `credentials_editor` (WITH ADMIN OPTION) | Laisse Vault rattacher les users dynamiques Ă ce rĂŽle |
+| `postgresql_database` | `` | La base (owner `_role`, `template0`, `alter_object_ownership`) |
+| `postgresql_function` | `user_lookup()` (dans la base ``) | Authentification pgbouncer (lit `pg_shadow`) |
+| `postgresql_grant` | EXECUTE sur `user_lookup` â `pgbouncer_auth` | Autorise pgbouncer Ă rĂ©soudre les users |
+
+Extrait clé :
+
+```hcl
+resource "postgresql_role" "app_role" {
+ for_each = var.applications
+ name = "${each.value}_role"
+ login = false # non-login : ne sert que de "porteur de droits"
+}
+resource "postgresql_database" "app_db" {
+ for_each = var.applications
+ name = each.value
+ owner = postgresql_role.app_role[each.value].name
+ template = "template0"
+ alter_object_ownership = true
+}
+```
+
+## Comment c'est appliqué
+
+Le workflow [`factory/.gitea/workflows/postgres.yaml`](https://gitea.arcodange.lab/arcodange-org/factory/src/branch/main/.gitea/workflows/postgres.yaml) se déclenche sur tout changement de `postgres/**/*.tf` ou `*.tfvars` :
+
+```mermaid
+%%{init: {'theme': 'base'}}%%
+flowchart LR
+ classDef ci fill:#059669,stroke:#047857,color:#fff
+ classDef db fill:#2563eb,stroke:#1e40af,color:#fff
+ PUSH["push tfvars"]:::ci --> JWT["OIDC Gitea â JWT Vault
(role gitea_cicd)"]:::ci
+ JWT --> READ["lit kvv1/postgres/credentials
â TF_VAR_postgres_*"]:::ci
+ READ --> APPLY["tofu apply postgres/iac"]:::ci
+ APPLY --> DB["base âčappâș + âčappâș_role"]:::db
+```
+
+Le provider PostgreSQL pointe l'hĂŽte `192.168.1.202` (`sslmode=disable`, `superuser=true`) et s'authentifie avec le compte `credentials_editor`, dont les identifiants sont dans Vault Ă `kvv1/postgres/credentials_editor/credentials`.
+
+## Le modĂšle de connexion (Ă retenir)
+
+> [!IMPORTANT]
+> L'application **ne se connecte pas** directement à Postgres avec un user fixe. Elle vise **`pgbouncer.tools:5432`** et utilise des **users dynamiques courts** émis par Vault, qui héritent de `_role` (donc des droits sur la base ``). C'est l'étape 4 (chart + VSO) et l'étape 5 (rÎle Vault `creds/`) qui cùblent ça. Ici, on ne fait qu'établir *la base et le rÎle propriétaire*.
+
+## Notes / contraintes
+
+- `credentials_editor` est un compte unique partagé par toutes les apps, à fort privilÚge (il peut créer des rÎles). Il sert aussi de compte de connexion au moteur Postgres de Vault (cf. [étape 3](03-vault-platform.md)).
+- La fonction `user_lookup()` est indispensable au mode `auth_query` de pgbouncer ; elle est `security_definer` et n'est exécutable que par `pgbouncer_auth`.
+
+## Related
+
+- [3. Vault plateforme](03-vault-platform.md) â la connexion VaultâPostgres rĂ©utilise `credentials_editor` ; Ă faire en parallĂšle.
+- [5. Terraform de l'app](05-app-terraform.md) â le module `app_roles` fait `GRANT _role TO âŠ` : il **exige** que `_role` existe dĂ©jĂ (créé ici).
+- [4. Chart Helm](04-helm-chart.md) â oĂč la connexion `pgbouncer.tools` + creds dynamiques est configurĂ©e.
diff --git a/doc/runbooks/new-web-app/03-vault-platform.md b/doc/runbooks/new-web-app/03-vault-platform.md
new file mode 100644
index 0000000..164d8ea
--- /dev/null
+++ b/doc/runbooks/new-web-app/03-vault-platform.md
@@ -0,0 +1,89 @@
+[Factory](../../../README.md) > [Doc](../../README.md) > [Runbooks](../README.md) > [Nouvelle application web](README.md) > **3. Vault plateforme**
+
+# 3. Déclarer l'app cÎté Vault plateforme
+
+> **Status:** â
Active
+> **Upstream:** [1. DépÎt Gitea](01-gitea-repo.md)
+> **Downstream:** [5. Terraform de l'app](05-app-terraform.md), [6. Workflows CI](06-ci-workflows.md)
+> **Related:** [2. Base de données](02-database.md) · [4. Chart Helm](04-helm-chart.md) · [Conventions de nommage](conventions.md)
+
+---
+
+## Summary
+
+Avant que la CI de l'app puisse gérer ses propres secrets, Vault doit connaßtre l'app : il lui faut un **rÎle JWT de CI** (`gitea_cicd_`) pour que la pipeline s'authentifie, une **policy CI** (`-ops`) qui l'autorise à créer ses rÎles Postgres/K8s, et une **policy runtime** (``) que le pod utilisera. Tout ça est généré par un module, depuis une seule ligne ajoutée cÎté plateforme dans le dépÎt `tools`.
+
+## Action
+
+Ajouter une entrée pour l'app au set `applications` de [`tools/hashicorp-vault/iac/terraform.tfvars`](https://gitea.arcodange.lab/arcodange-org/tools/src/branch/main/hashicorp-vault/iac/terraform.tfvars) :
+
+```hcl
+applications = [
+ { name = "webapp" },
+ { name = "erp" },
+ { name = "" }, # â ajouter
+ # options possibles :
+ # {
+ # name = ""
+ # ops_policies = ["factory__cf_r2_arcodange_tf"] # policies ops supplémentaires (ex. token Cloudflare)
+ # service_account_names = ["cloudflared"] # SA additionnels autorisés à prendre la policy runtime
+ # service_account_namespaces = ["tools"] # namespaces additionnels
+ # },
+]
+```
+
+Puis pousser : la CI [`tools/.gitea/workflows/vault.yaml`](https://gitea.arcodange.lab/arcodange-org/tools/src/branch/main/.gitea/workflows/vault.yaml) applique le Terraform.
+
+## Ce que ça crée
+
+Le module [`app_policy`](https://gitea.arcodange.lab/arcodange-org/tools/src/branch/main/hashicorp-vault/iac/modules/app_policy/main.tf) (appelé en `for_each` depuis [`main.tf`](https://gitea.arcodange.lab/arcodange-org/tools/src/branch/main/hashicorp-vault/iac/main.tf)) crée, **par app** :
+
+| Ressource Vault | Nom | à quoi ça sert |
+|---|---|---|
+| `vault_jwt_auth_backend_role` | `gitea_cicd_` | **Identité de la CI** : la pipeline de l'app s'y authentifie (mount `gitea_jwt`, `user_claim=email`, `bound_audiences=[gitea_app_id]`) |
+| `vault_policy` (ops) | `-ops` | Droits CI : créer `postgres/roles/*`, `auth/kubernetes/role/*`, éditer `kvv2//*`, lire les secrets de bootstrap google/gitea |
+| `vault_identity_group` | `-ops` | Groupe Vault rattachant les comptes Gitea Ă la policy ops |
+| `vault_policy` (runtime) | `` | Droits **du pod** : lire `kvv2/data//*` et `postgres/creds/*` |
+
+Extraits clés :
+
+```hcl
+resource "vault_jwt_auth_backend_role" "gitea_jwt_cicd" {
+ backend = data.vault_auth_backend.gitea_jwt.path # "gitea_jwt"
+ role_name = "gitea_cicd_${local.name}"
+ token_policies = concat(["default"], var.ops_policies)
+ bound_audiences = [var.gitea_app_id]
+ user_claim = "email"
+ role_type = "jwt"
+}
+
+resource "vault_policy" "app" { # policy runtime du pod
+ name = local.name # = ""
+ policy = data.vault_policy_document.app.hcl # read kvv2/data//* + postgres/creds/*
+}
+```
+
+## Prérequis plateforme (déjà là )
+
+Ces fondations vivent dans [`tools/hashicorp-vault/iac/main.tf`](https://gitea.arcodange.lab/arcodange-org/tools/src/branch/main/hashicorp-vault/iac/main.tf) et n'ont **pas** Ă ĂȘtre recréées :
+
+- mounts `kvv2` (KV v2), `postgres` (moteur de bases de données), `transit` (cache VSO), auth `kubernetes` ;
+- la **connexion VaultâPostgres** (`vault_database_secret_backend_connection`) qui se connecte Ă `pgbouncer.tools:5432/postgres` avec le compte `credentials_editor` (issu de l'[Ă©tape 2](02-database.md)) ;
+- `var.gitea_app_id` = l'id de l'application OAuth2 Gitea, réglé une fois au setup ([`gitea_oidc_auth.yml`](https://gitea.arcodange.lab/arcodange-org/factory/src/branch/main/ansible/arcodange/factory/playbooks/tools/roles/hashicorp_vault/tasks/gitea_oidc_auth.yml)).
+
+## Pourquoi cette étape vient avant la CI de l'app
+
+> [!IMPORTANT]
+> C'est ici que `gitea_cicd_` **naßt**. Le `iac/providers.tf` de l'app ([étape 5](05-app-terraform.md)) et le step `vault-action` du workflow ([étape 6](06-ci-workflows.md)) s'authentifient avec ce rÎle. S'il n'existe pas encore, la toute premiÚre exécution de la CI de l'app échoue à l'authentification Vault. **Appliquer cette étape (et l'[étape 2](02-database.md)) avant de pousser le `iac/` de l'app.**
+
+## Notes / contraintes
+
+- Découpage des privilÚges : la policy **`-ops`** (CI, large) est distincte de la policy **``** (runtime, en lecture seule sur ses propres secrets). Le pod ne peut jamais créer de rÎles.
+- `ops_policies` permet d'octroyer Ă la CI des droits transverses (ex. `cms` lit un token Cloudflare R2 via `factory__cf_r2_arcodange_tf`).
+
+## Related
+
+- [2. Base de donnĂ©es](02-database.md) â fournit `credentials_editor`, rĂ©utilisĂ© par la connexion VaultâPostgres.
+- [5. Terraform de l'app](05-app-terraform.md) â s'authentifie avec `gitea_cicd_` et crĂ©e `creds/` + le rĂŽle K8s ``.
+- [6. Workflows CI](06-ci-workflows.md) â le step `vault-action` et `tofu apply` utilisent `gitea_cicd_`.
+- [4. Chart Helm](04-helm-chart.md) â le pod utilise la policy runtime `` via son ServiceAccount.
diff --git a/doc/runbooks/new-web-app/04-helm-chart.md b/doc/runbooks/new-web-app/04-helm-chart.md
new file mode 100644
index 0000000..24cbd51
--- /dev/null
+++ b/doc/runbooks/new-web-app/04-helm-chart.md
@@ -0,0 +1,183 @@
+[Factory](../../../README.md) > [Doc](../../README.md) > [Runbooks](../README.md) > [Nouvelle application web](README.md) > **4. Chart Helm**
+
+# 4. Le chart Helm de l'application
+
+> **Status:** â
Active
+> **Upstream:** [5. Terraform de l'app](05-app-terraform.md) (crée les rÎles Vault que le chart consomme)
+> **Downstream:** [7. Enregistrement ArgoCD](07-argocd-register.md) (déploie ce chart)
+> **Related:** [2. Base de données](02-database.md) · [3. Vault plateforme](03-vault-platform.md) · [6. Workflows CI](06-ci-workflows.md) · [Conventions de nommage](conventions.md)
+
+---
+
+## Summary
+
+Le dossier `chart/` est l'**unité de déploiement** : c'est lui qu'ArgoCD applique (`path: chart`). Il décrit le Deployment, le Service, l'Ingress, et surtout le cùblage des secrets via le **Vault Secrets Operator (VSO)** : pas un mot de passe en clair, des identifiants Postgres **dynamiques** injectés à l'exécution. On part d'un `helm create ` puis on ajoute les CRD Vault, la ConfigMap, et on ajuste l'ingress.
+
+## Structure du chart
+
+```
+chart/
+âââ Chart.yaml # name: , version, appVersion (= tag image par dĂ©faut)
+âââ values.yaml # image, service, ingress, serviceAccount, autoscalingâŠ
+âââ templates/
+ âââ deployment.yaml # consomme vso-db-credentials + secretkv
+ âââ service.yaml # ClusterIP
+ âââ ingress.yaml # Traefik (voir patterns ci-dessous)
+ âââ serviceaccount.yaml # SA (serviceAccount.create: true)
+ âââ config.yaml # ConfigMap : env non-secrets (host DB = pgbouncer)
+ âââ vaultauth.yaml # VaultAuth : SA â rĂŽle Vault
+ âââ vaultdynamicsecret.yaml # creds Postgres dynamiques (postgres/creds/)
+ âââ vaultsecret.yaml # config statique (kvv2//config)
+ âââ hpa.yaml # dĂ©sactivĂ© par dĂ©faut
+ âââ _helpers.tpl # name/fullname/labels
+ âââ NOTES.txt
+```
+
+> [!TIP]
+> Bootstrap : `helm create ` génÚre deployment/service/ingress/serviceaccount/hpa/_helpers/NOTES. Il reste à **ajouter** `config.yaml` (ConfigMap), les 3 CRD Vault (`vaultauth`, `vaultdynamicsecret`, `vaultsecret`), et à **ajuster** `values.yaml` (image, ingress) + `deployment.yaml` (envFrom). Copier ceux d'[`erp/chart`](https://gitea.arcodange.lab/arcodange-org/erp/src/branch/main/chart) ou [`webapp/chart`](https://gitea.arcodange.lab/arcodange-org/webapp/src/branch/main/chart) est le plus rapide.
+
+## Connexion Ă la base : via pgbouncer, jamais en direct
+
+La ConfigMap pointe **`pgbouncer.tools`** (le pooler dans le namespace `tools`), port 5432, base ``. Pas de host Postgres en dur, pas d'utilisateur statique.
+
+```yaml
+# chart/templates/config.yaml â exemple erp
+data:
+ DOLI_DB_TYPE: pgsql
+ DOLI_DB_HOST: pgbouncer.tools # â le pooler, pas postgres directement
+ DOLI_DB_HOST_PORT: !!str 5432
+ DOLI_DB_NAME: erp # =
+```
+
+```yaml
+# chart/templates/config.yaml â exemple webapp (chaĂźne de connexion)
+data:
+ DATABASE_URL: postgres://pgbouncer_auth:pgbouncer_auth@pgbouncer.tools/postgres?sslmode=disable
+```
+
+Le **vrai** user/mot de passe vient du Secret K8s `vso-db-credentials` (voir ci-dessous), pas de la ConfigMap.
+
+## Les secrets via VSO (le cĆur)
+
+Trois CRD du Vault Secrets Operator (extraits du chart `erp`) :
+
+```yaml
+# vaultauth.yaml â authentifie le pod auprĂšs de Vault
+apiVersion: secrets.hashicorp.com/v1beta1
+kind: VaultAuth
+spec:
+ method: kubernetes
+ mount: kubernetes
+ kubernetes:
+ role: erp # = rÎle K8s Vault (créé à l'étape 5)
+ serviceAccount: {{ include "erp.serviceAccountName" . }}
+ audiences: [vault]
+```
+
+```yaml
+# vaultdynamicsecret.yaml â identifiants Postgres dynamiques
+apiVersion: secrets.hashicorp.com/v1beta1
+kind: VaultDynamicSecret
+spec:
+ mount: postgres
+ path: creds/erp # = postgres/creds/
+ destination:
+ create: true
+ name: vso-db-credentials # Secret K8s consommé par le Deployment
+ rolloutRestartTargets:
+ - kind: Deployment
+ name: {{ include "erp.fullname" . }} # redémarre le pod à chaque rotation
+ vaultAuthRef: auth
+```
+
+```yaml
+# vaultsecret.yaml â config statique (mots de passe admin initiaux, etc.)
+apiVersion: secrets.hashicorp.com/v1beta1
+kind: VaultStaticSecret
+spec:
+ type: kv-v2
+ mount: kvv2
+ path: erp/config # = kvv2//config (rempli à l'étape 5)
+ destination:
+ name: secretkv
+ create: true
+ refreshAfter: 24h
+ vaultAuthRef: auth
+```
+
+Le Deployment consomme ensuite `vso-db-credentials` (clés `username`/`password`) et `secretkv` (via `envFrom`/`secretKeyRef`).
+
+### Flux runtime
+
+```mermaid
+%%{init: {'theme': 'base'}}%%
+flowchart LR
+ classDef pod fill:#b45309,stroke:#92400e,color:#fff
+ classDef vault fill:#7c3aed,stroke:#6d28d9,color:#fff
+ classDef db fill:#2563eb,stroke:#1e40af,color:#fff
+
+ SA["Pod · SA âčappâș"]:::pod
+ VA["VaultAuth
role âčappâș (k8s)"]:::vault
+ VDS["VaultDynamicSecret
postgres/creds/âčappâș"]:::vault
+ VSS["VaultStaticSecret
kvv2/âčappâș/config"]:::vault
+ SEC["Secret K8s
vso-db-credentials + secretkv"]:::pod
+ PGB["pgbouncer.tools:5432"]:::db
+ DB["base âčappâș
(user dynamique â âčappâș_role)"]:::db
+
+ SA --> VA
+ VA --> VDS
+ VA --> VSS
+ VDS --> SEC
+ VSS --> SEC
+ SEC --> SA
+ SA --> PGB --> DB
+```
+
+> [!IMPORTANT]
+> `serviceAccount.create` doit valoir `true` dans `values.yaml` : c'est ce SA `` que `VaultAuth` lie au rĂŽle Vault ``. Sans lui, pas d'authentification, donc pas de creds DB.
+
+## Ingress : interne `.lab` vs public `.fr`
+
+Deux patterns selon l'exposition voulue (annotations Traefik dans `values.yaml`) :
+
+| | Interne (`.lab`) â ex. `erp` | Public (`.fr`) â ex. `webapp` |
+|---|---|---|
+| `entrypoints` | `websecure` | `web` |
+| TLS | `router.tls: "true"` + `certresolver: letsencrypt` | (terminé en amont) |
+| Middleware | `localIp@file` (restreint au LAN) | `kube-system-crowdsec@kubernetescrd` (WAF) |
+| `nodeSelector` | â | `kubernetes.io/hostname: pi1` (garde l'IP source, point d'entrĂ©e rĂ©seau) |
+| HĂŽte | `.arcodange.lab` | `.arcodange.fr` |
+
+```yaml
+# values.yaml â ingress interne .lab (extrait erp)
+ingress:
+ enabled: true
+ annotations:
+ traefik.ingress.kubernetes.io/router.entrypoints: websecure
+ traefik.ingress.kubernetes.io/router.tls: "true"
+ traefik.ingress.kubernetes.io/router.tls.certresolver: letsencrypt
+ traefik.ingress.kubernetes.io/router.middlewares: localIp@file
+ hosts:
+ - host: erp.arcodange.lab
+ paths: [{ path: /, pathType: Prefix }]
+```
+
+## Image
+
+| Cas | `image.repository` | Exemple |
+|---|---|---|
+| Image **maison** (build dans le dépÎt) | `gitea.arcodange.lab/arcodange-org/` | `webapp` (cf. [étape 6](06-ci-workflows.md)) |
+| Image **publique** | l'image upstream | `erp` â `dolibarr/dolibarr` |
+
+## Notes / contraintes
+
+- Si l'app stocke des fichiers, ajouter un `pvc.yaml` (`storageClassName: longhorn`, `accessModes: [ReadWriteMany]`, annotation `helm.sh/resource-policy: keep` pour survivre Ă un `helm uninstall`) â cf. `erp/chart/templates/pvc.yaml`.
+- Les CRD VSO supposent que le VSO tourne dans le cluster (déployé par `tools`) et que le rÎle K8s `` + le rÎle `creds/` existent (étape 5).
+
+## Related
+
+- [5. Terraform de l'app](05-app-terraform.md) â crĂ©e `postgres/creds/`, le rĂŽle K8s `` et remplit `kvv2//config` que ces CRD consomment.
+- [2. Base de donnĂ©es](02-database.md) â la base `` et `pgbouncer.tools` ciblĂ©s ici.
+- [3. Vault plateforme](03-vault-platform.md) â la policy runtime `` qu'utilise `VaultAuth`.
+- [6. Workflows CI](06-ci-workflows.md) â construit l'image rĂ©fĂ©rencĂ©e par `image.repository`.
+- [RĂ©fĂ©rence VSO/secrets faisant autoritĂ©](https://gitea.arcodange.lab/arcodange-org/tools/src/branch/main/hashicorp-vault/iac/modules/README.md) â explication dĂ©taillĂ©e VaultConnection/VaultAuth/VaultDynamicSecret cĂŽtĂ© `tools` (Ă ne pas dupliquer ici).
diff --git a/doc/runbooks/new-web-app/05-app-terraform.md b/doc/runbooks/new-web-app/05-app-terraform.md
new file mode 100644
index 0000000..a5b98e5
--- /dev/null
+++ b/doc/runbooks/new-web-app/05-app-terraform.md
@@ -0,0 +1,96 @@
+[Factory](../../../README.md) > [Doc](../../README.md) > [Runbooks](../README.md) > [Nouvelle application web](README.md) > **5. Terraform de l'app**
+
+# 5. Le Terraform de l'application
+
+> **Status:** â
Active
+> **Upstream:** [2. Base de données](02-database.md), [3. Vault plateforme](03-vault-platform.md)
+> **Downstream:** [4. Chart Helm](04-helm-chart.md), [6. Workflows CI](06-ci-workflows.md)
+> **Related:** [Conventions de nommage](conventions.md)
+
+---
+
+## Summary
+
+Le `iac/` du dépÎt déclare les ressources Vault **propres à l'app** : un rÎle Postgres dynamique (`postgres/creds/`), un rÎle d'authentification Kubernetes (``), et les secrets de config KV. Le gros du travail est fait par un **module partagé** (`app_roles`, dans `tools`) ; le dépÎt se contente de l'appeler avec son nom et d'ajouter ses secrets spécifiques.
+
+## Les trois fichiers
+
+### `providers.tf` â s'authentifier Ă Vault avec le rĂŽle CI de l'app
+
+```hcl
+terraform {
+ required_providers {
+ vault = { source = "vault", version = "4.4.0" }
+ }
+}
+provider "vault" {
+ address = "https://vault.arcodange.lab"
+ auth_login_jwt { # JWT fourni par la CI via TERRAFORM_VAULT_AUTH_JWT
+ mount = "gitea_jwt"
+ role = "gitea_cicd_" # â créé Ă l'Ă©tape 3 ; DOIT exister avant le 1er apply
+ }
+}
+```
+
+### `backend.tf` â Ă©tat distant sur GCS, prĂ©fixe par app
+
+```hcl
+terraform {
+ backend "gcs" {
+ bucket = "arcodange-tf"
+ prefix = "/main" # â un prĂ©fixe d'Ă©tat dĂ©diĂ© par app
+ }
+}
+```
+
+### `main.tf` â appeler le module partagĂ© + secrets de l'app
+
+```hcl
+module "app_roles" {
+ source = "git::ssh://git@192.168.1.202:2222/arcodange-org/tools.git//hashicorp-vault/iac/modules/app_roles?depth=1&ref=main"
+ name = ""
+ # database = "" # optionnel ; par défaut = name
+}
+
+# Exemple : secrets de config statiques de l'app, écrits dans kvv2//config
+resource "vault_kv_secret_v2" "config" {
+ mount = module.app_roles.mount_paths.kvv2 # "kvv2"
+ name = format("%sconfig", module.app_roles.kvv2_path_prefix) # "/config"
+ data_json = jsonencode({
+ # ⊠clĂ©s propres Ă l'app (ex. erp : DOLI_ADMIN_LOGIN/PASSWORD, DOLI_INSTANCE_UNIQUE_ID) âŠ
+ })
+}
+```
+
+## Ce que le module `app_roles` crée
+
+[`modules/app_roles/main.tf`](https://gitea.arcodange.lab/arcodange-org/tools/src/branch/main/hashicorp-vault/iac/modules/app_roles/main.tf) :
+
+| Ressource | Effet |
+|---|---|
+| `vault_database_secret_backend_role` â `postgres/creds/` | Ă chaque demande : `CREATE ROLE "âŠ" LOGIN PASSWORD ⊠VALID UNTIL ⊠; GRANT _role TO "âŠ"`. Le user Ă©phĂ©mĂšre **hĂ©rite** de `_role` (donc des droits sur la base). Ă la rĂ©vocation : `REASSIGN OWNED ⊠TO _role` + `REVOKE`. |
+| `vault_kubernetes_auth_backend_role` â `` | Lie le SA `` du namespace `` aux policies `default` + `` (TTL 3600 s). C'est ce que `VaultAuth` cible (Ă©tape 4). |
+
+Sorties utiles : `mount_paths` (`{k8s, pg, kvv2}`), `kvv2_path_prefix` (`/`), `name`, `database`.
+
+## Dépendances (à respecter)
+
+> [!IMPORTANT]
+> Ce Terraform **suppose** que deux choses existent déjà :
+> - le rĂŽle Postgres `_role` (créé Ă l'[Ă©tape 2](02-database.md)) â sinon le `GRANT _role TO âŠ` du rĂŽle dynamique est invalide ;
+> - le rĂŽle JWT `gitea_cicd_` et la policy `` (créés Ă l'[Ă©tape 3](03-vault-platform.md)) â sinon l'authentification du provider Ă©choue / le rĂŽle K8s ne peut rĂ©fĂ©rencer la policy.
+>
+> Ce `iac/` est appliqué par la CI de l'app, voir [étape 6](06-ci-workflows.md). Ne pas pousser ce dossier avant d'avoir appliqué les étapes 2 et 3.
+
+## Notes / contraintes
+
+- Le module est récupéré **en SSH** via le bot `tofu_module_reader` (cf. [étape 1](01-gitea-repo.md)) ; `?ref=main&depth=1` épingle la branche et limite le clone.
+- L'état est isolé par `prefix = "/main"` : pas de collision entre apps dans le bucket `arcodange-tf`.
+- `erp` et `webapp` montrent deux variantes : `erp` passe par `module "app_roles"` ; `webapp` inline encore les ressources (`vault_database_secret_backend_role` + `vault_kubernetes_auth_backend_role`) â prĂ©fĂ©rer le module pour une nouvelle app.
+
+## Related
+
+- [2. Base de donnĂ©es](02-database.md) â fournit `_role`.
+- [3. Vault plateforme](03-vault-platform.md) â fournit `gitea_cicd_` et la policy ``.
+- [4. Chart Helm](04-helm-chart.md) â consomme `postgres/creds/`, le rĂŽle K8s `` et `kvv2//config`.
+- [6. Workflows CI](06-ci-workflows.md) â applique ce `iac/`.
diff --git a/doc/runbooks/new-web-app/06-ci-workflows.md b/doc/runbooks/new-web-app/06-ci-workflows.md
new file mode 100644
index 0000000..999125f
--- /dev/null
+++ b/doc/runbooks/new-web-app/06-ci-workflows.md
@@ -0,0 +1,108 @@
+[Factory](../../../README.md) > [Doc](../../README.md) > [Runbooks](../README.md) > [Nouvelle application web](README.md) > **6. Workflows CI**
+
+# 6. Les workflows CI (`.gitea/workflows/`)
+
+> **Status:** â
Active
+> **Upstream:** [1. DépÎt Gitea](01-gitea-repo.md) (secrets d'org), [3. Vault plateforme](03-vault-platform.md) (`gitea_cicd_`)
+> **Related:** [4. Chart Helm](04-helm-chart.md) · [5. Terraform de l'app](05-app-terraform.md) · [7. Enregistrement ArgoCD](07-argocd-register.md) · [Conventions de nommage](conventions.md)
+
+---
+
+## Summary
+
+Deux workflows Gitea Actions vivent dans le dĂ©pĂŽt : **`vault.yaml`** applique le Terraform de l'app (`iac/`) en s'authentifiant Ă Vault via OIDC, et **`dockerimage.yaml`** (optionnel) construit l'image et la pousse au registre Gitea. Le dĂ©ploiement lui-mĂȘme n'est pas dans la CI : c'est ArgoCD qui s'en charge ([Ă©tape 7](07-argocd-register.md)).
+
+## `vault.yaml` â appliquer le `iac/` de l'app
+
+Déclenché sur tout changement de `iac/*.tf`. Deux jobs : obtenir un JWT depuis Gitea, puis `tofu apply`.
+
+```yaml
+on:
+ workflow_dispatch: {}
+ push: { paths: ['iac/*.tf'] }
+ pull_request: { paths: ['iac/*.tf'] }
+
+# job 1 : Ă©change OIDC Gitea â JWT (script base64 fourni en secret d'org)
+# run: echo -n "${{ secrets.vault_oauth__sh_b64 }}" | base64 -d | bash
+
+# job 2 : lire les secrets de bootstrap puis appliquer
+- name: read vault secret
+ uses: https://gitea.arcodange.lab/arcodange-org/vault-action.git@main
+ with:
+ url: https://vault.arcodange.lab
+ caCertificate: ${{ secrets.HOMELAB_CA_CERT }}
+ jwtGiteaOIDC: ${{ needs.gitea_vault_auth.outputs.gitea_vault_jwt }}
+ role: gitea_cicd_ # â le rĂŽle JWT de l'app (Ă©tape 3)
+ method: jwt
+ path: gitea_jwt
+ secrets: |
+ kvv1/google/credentials credentials | GOOGLE_BACKEND_CREDENTIALS ;
+ kvv1/gitea/tofu_module_reader ssh_private_key | TERRAFORM_SSH_KEY ;
+- uses: actions/checkout@v4
+- name: terraform apply
+ uses: dflook/terraform-apply@v1
+ with: { path: iac, auto_approve: true }
+```
+
+Les deux secrets lus servent au backend (clé GCS `GOOGLE_BACKEND_CREDENTIALS`) et au clone du module partagé en SSH (`TERRAFORM_SSH_KEY`, cf. [étape 5](05-app-terraform.md)).
+
+> [!WARNING]
+> **PiĂšge du `role:` copiĂ©-collĂ©.** Le `role:` du step `vault-action` **et** le `role` de `iac/providers.tf` doivent tous deux ĂȘtre `gitea_cicd_`. L'exemple `erp` porte encore `role: gitea_cicd_webapp` dans son [`vault.yaml`](https://gitea.arcodange.lab/arcodange-org/erp/src/branch/main/.gitea/workflows/vault.yaml) (reliquat de copier-coller) alors que son `providers.tf` utilise bien `gitea_cicd_erp`. VĂ©rifie et aligne sur le nom de **ton** app, sinon la CI lit/Ă©crit avec la mauvaise identitĂ©.
+
+## `dockerimage.yaml` â construire l'image (si image maison)
+
+à n'ajouter **que** si l'app build sa propre image (pas pour une image publique comme `erp`/Dolibarr). Déclenché au push sur `main`, en ignorant `README.md` et `chart/**` (changer le chart ne reconstruit pas l'image).
+
+```yaml
+on:
+ push: { branches: [main], paths-ignore: ['README.md', 'chart/**'] }
+
+jobs:
+ build-and-push-image:
+ steps:
+ - uses: docker/login-action@v3
+ with:
+ registry: gitea.arcodange.lab
+ username: ${{ github.actor }}
+ password: ${{ secrets.PACKAGES_TOKEN }} # secret d'org (étape 1)
+ - uses: actions/checkout@v4
+ - run: |
+ TAGS="latest ${{ github.ref_name }}"
+ docker build -t app .
+ for TAG in $TAGS; do
+ docker tag app gitea.arcodange.lab/${{ github.repository }}:$TAG
+ docker push gitea.arcodange.lab/${{ github.repository }}:$TAG
+ done
+```
+
+L'image atterrit donc en `gitea.arcodange.lab/arcodange-org/:latest` â exactement ce que `image.repository` du chart rĂ©fĂ©rence ([Ă©tape 4](04-helm-chart.md)). Un `Dockerfile` multi-stage Ă la racine convient (cf. [`webapp/Dockerfile`](https://gitea.arcodange.lab/arcodange-org/webapp/src/branch/main/Dockerfile)).
+
+## Vue d'ensemble
+
+```mermaid
+%%{init: {'theme': 'base'}}%%
+flowchart TB
+ classDef ci fill:#059669,stroke:#047857,color:#fff
+ classDef out fill:#7c3aed,stroke:#6d28d9,color:#fff
+ PUSH["push sur main"]:::ci
+ PUSH -->|"iac/*.tf modifié"| TF["vault.yaml
OIDC â JWT â tofu apply iac/"]:::ci
+ PUSH -->|"code modifié"| IMG["dockerimage.yaml
build + push image"]:::ci
+ TF --> VR["creds/âčappâș + rĂŽle K8s âčappâș + KV"]:::out
+ IMG --> REG["registre gitea.arcodange.lab/arcodange-org/âčappâș"]:::out
+```
+
+## Déploiement automatique sur nouvelle image
+
+Pour qu'ArgoCD redéploie quand une nouvelle image est poussée, on n'ajoute **rien** dans la CI : ce sont les annotations `argocd-image-updater` posées à l'[étape 7](07-argocd-register.md) (stratégie `digest`) qui surveillent le tag `latest`.
+
+## Notes / contraintes
+
+- `concurrency: cancel-in-progress` est activĂ© sur les deux workflows : un nouveau push annule le run prĂ©cĂ©dent sur la mĂȘme ref.
+- Le `vault-action` est lui-mĂȘme un dĂ©pĂŽt Gitea (`arcodange-org/vault-action`) Ă©pinglĂ© `@main`.
+
+## Related
+
+- [3. Vault plateforme](03-vault-platform.md) â d'oĂč vient `gitea_cicd_`.
+- [5. Terraform de l'app](05-app-terraform.md) â ce que `vault.yaml` applique.
+- [4. Chart Helm](04-helm-chart.md) â `image.repository` = l'image poussĂ©e ici.
+- [7. Enregistrement ArgoCD](07-argocd-register.md) â dĂ©ploie, et porte les annotations d'auto-update.
diff --git a/doc/runbooks/new-web-app/07-argocd-register.md b/doc/runbooks/new-web-app/07-argocd-register.md
new file mode 100644
index 0000000..9f54536
--- /dev/null
+++ b/doc/runbooks/new-web-app/07-argocd-register.md
@@ -0,0 +1,91 @@
+[Factory](../../../README.md) > [Doc](../../README.md) > [Runbooks](../README.md) > [Nouvelle application web](README.md) > **7. Enregistrement ArgoCD**
+
+# 7. Enregistrer l'app dans ArgoCD
+
+> **Status:** â
Active
+> **Upstream:** [4. Chart Helm](04-helm-chart.md), [6. Workflows CI](06-ci-workflows.md)
+> **Related:** [Conventions de nommage](conventions.md) · [Checklist](08-checklist.md)
+
+---
+
+## Summary
+
+ArgoCD fonctionne en **app-of-apps** : le chart `factory/argocd` lit une liste d'applications dans son `values.yaml` et génÚre une ressource `Application` par entrée. Enregistrer l'app = ajouter son nom à cette liste. ArgoCD se charge ensuite de cloner le dépÎt, déployer `chart/` dans le namespace ``, et resynchroniser à chaque push.
+
+## Action
+
+Ajouter `` sous `gitea_applications` dans [`factory/argocd/values.yaml`](https://gitea.arcodange.lab/arcodange-org/factory/src/branch/main/argocd/values.yaml) :
+
+```yaml
+gitea_applications:
+ webapp: { ⊠}
+ erp:
+ annotations: {}
+ : # â cas simple
+ annotations: {}
+```
+
+Variante avec **auto-déploiement** sur nouvelle image (recommandé pour une image maison) :
+
+```yaml
+ :
+ annotations:
+ argocd-image-updater.argoproj.io/image-list: =gitea.arcodange.lab/arcodange-org/:latest
+ argocd-image-updater.argoproj.io/.update-strategy: digest
+```
+
+Options supplémentaires :
+
+| Champ | Quand l'utiliser | Effet |
+|---|---|---|
+| `org: arcodange` | dépÎt hors `arcodange-org` | change le `repoURL` (défaut `arcodange-org`) |
+| `syncPolicy: âŠ` | contrĂŽle manuel | surcharge la policy (dĂ©faut : `automated {prune, selfHeal}`) |
+
+## Ce que ça génÚre
+
+Le template [`argocd/templates/apps.yaml`](https://gitea.arcodange.lab/arcodange-org/factory/src/branch/main/argocd/templates/apps.yaml) rend, pour chaque entrée :
+
+```yaml
+apiVersion: argoproj.io/v1alpha1
+kind: Application
+metadata:
+ name:
+ namespace: argocd
+spec:
+ project: default
+ source:
+ repoURL: https://gitea.arcodange.lab/arcodange-org/ # = arcodange-org par défaut
+ targetRevision: HEAD
+ path: chart # â d'oĂč l'exigence du dossier chart/
+ destination:
+ server: https://kubernetes.default.svc
+ namespace: # = nom de l'app
+ syncPolicy:
+ automated: { prune: true, selfHeal: true }
+ syncOptions: [ CreateNamespace=true ] # le namespace est créé tout seul
+```
+
+```mermaid
+%%{init: {'theme': 'base'}}%%
+flowchart LR
+ classDef gitops fill:#7c3aed,stroke:#6d28d9,color:#fff
+ classDef k8s fill:#2563eb,stroke:#1e40af,color:#fff
+ VAL["values.yaml
gitea_applications.âčappâș"]:::gitops --> APP["Application ArgoCD
âčappâș"]:::gitops
+ APP -->|"path: chart, HEAD"| SYNC["sync du dépÎt
arcodange-org/âčappâș"]:::gitops
+ SYNC --> NS["namespace âčappâș
(CreateNamespace=true)"]:::k8s
+ NS --> DEP["Deployment + Service + Ingress + CRD Vault"]:::k8s
+```
+
+## Notes / contraintes
+
+> [!IMPORTANT]
+> `path: chart` et `namespace: ` sont **dĂ©duits du nom**, pas configurables par entrĂ©e. C'est pourquoi le dossier doit s'appeler `chart/` ([Ă©tape 1](01-gitea-repo.md)) et le nom doit ĂȘtre cohĂ©rent partout ([conventions](conventions.md)).
+
+- Le chart `factory/argocd` est lui-mĂȘme rĂ©conciliĂ© par ArgoCD (app-of-apps racine) : committer `values.yaml` sur `main` suffit Ă faire apparaĂźtre/synchroniser la nouvelle `Application`. Pas de `kubectl apply` manuel.
+- `prune: true` + `selfHeal: true` : ArgoCD supprime ce qui n'est plus dans le chart et réécrase les dérives manuelles. En tenir compte avant tout `kubectl edit`.
+
+## Related
+
+- [4. Chart Helm](04-helm-chart.md) â le contenu dĂ©ployĂ© (le dossier `chart/`).
+- [6. Workflows CI](06-ci-workflows.md) â les annotations `argocd-image-updater` collaborent avec l'image poussĂ©e.
+- [8. Checklist](08-checklist.md) â vĂ©rifier que l'`Application` passe `Healthy`/`Synced`.
diff --git a/doc/runbooks/new-web-app/08-checklist.md b/doc/runbooks/new-web-app/08-checklist.md
new file mode 100644
index 0000000..13ce186
--- /dev/null
+++ b/doc/runbooks/new-web-app/08-checklist.md
@@ -0,0 +1,77 @@
+[Factory](../../../README.md) > [Doc](../../README.md) > [Runbooks](../README.md) > [Nouvelle application web](README.md) > **8. Checklist**
+
+# 8. Checklist & Definition of Done
+
+> **Status:** â
Active
+> **Upstream:** [7. Enregistrement ArgoCD](07-argocd-register.md)
+> **Related:** [README du runbook](README.md) · [Conventions de nommage](conventions.md)
+
+---
+
+## Summary
+
+Récapitulatif imprimable de toute la procédure, dans l'ordre des dépendances. à cocher au fur et à mesure ; chaque ligne renvoie à sa page détaillée.
+
+## Ordre des dépendances
+
+```
+[01] DĂ©pĂŽt Gitea âââŹââ> [02] base + _role âââ
+ âââ> [03] gitea_cicd_ ââââ†(02 et 03 avant 05)
+ âŒ
+ [04+05+06] chart/ + iac/ + .gitea/ ââ push â CI
+ âŒ
+ [07] ArgoCD ââ dĂ©ploie ââ Runtime
+```
+
+## Checklist
+
+**Préparation**
+- [ ] Nom `` choisi (kebab-case minuscule), cohĂ©rent partout â [conventions](conventions.md)
+
+**1 · DĂ©pĂŽt** â [dĂ©tails](01-gitea-repo.md)
+- [ ] DépÎt `arcodange-org/` créé (hérite `HOMELAB_CA_CERT`, `vault_oauth__sh_b64`, `PACKAGES_TOKEN`)
+- [ ] Squelette en place : `chart/`, `iac/`, `.gitea/workflows/`, `.gitignore` (+ `Dockerfile` si image maison)
+
+**2 · Base de donnĂ©es** â [dĂ©tails](02-database.md)
+- [ ] `""` ajouté à `factory/postgres/iac/terraform.tfvars`
+- [ ] CI `factory` « Postgres » verte â base `` + rĂŽle `_role` créés
+
+**3 · Vault plateforme** â [dĂ©tails](03-vault-platform.md)
+- [ ] `{ name = "" }` ajouté à `tools/hashicorp-vault/iac/terraform.tfvars`
+- [ ] CI `tools` « Vault » verte â `gitea_cicd_`, policies `` / `-ops` créées
+
+> [!IMPORTANT]
+> Ne pas pousser le `iac/` de l'app (étape 5/6) tant que **2 et 3** ne sont pas appliquées : la CI Terraform de l'app en dépend.
+
+**4 · Chart Helm** â [dĂ©tails](04-helm-chart.md)
+- [ ] `Chart.yaml` (`name: `), `values.yaml` (image, ingress, `serviceAccount.create: true`)
+- [ ] `config.yaml` pointe `pgbouncer.tools` / base ``
+- [ ] `vaultauth.yaml` (role ``), `vaultdynamicsecret.yaml` (`creds/`), `vaultsecret.yaml` (`kvv2//config`)
+- [ ] Ingress choisi : interne `.lab` (websecure + letsencrypt + `localIp@file`) ou public `.fr` (web + crowdsec + `nodeSelector pi1`)
+
+**5 · Terraform de l'app** â [dĂ©tails](05-app-terraform.md)
+- [ ] `providers.tf` : role `gitea_cicd_`
+- [ ] `backend.tf` : prefix `/main`
+- [ ] `main.tf` : `module "app_roles"` (`name = ""`) + secrets `kvv2//config`
+
+**6 · Workflows CI** â [dĂ©tails](06-ci-workflows.md)
+- [ ] `vault.yaml` : `role: gitea_cicd_` **alignĂ©** (pas un reliquat copiĂ©-collĂ©) â ïž
+- [ ] `dockerimage.yaml` + `Dockerfile` (si image maison) â push `gitea.arcodange.lab/arcodange-org/`
+- [ ] Push â CI « vault » verte (`creds/` + rĂŽle K8s `` + KV créés), CI « image » verte
+
+**7 · ArgoCD** â [dĂ©tails](07-argocd-register.md)
+- [ ] `` ajouté sous `gitea_applications` dans `factory/argocd/values.yaml` (+ annotations image-updater si voulu)
+- [ ] Commit sur `main` â `Application` `` apparaĂźt dans ArgoCD
+
+## Definition of Done
+
+- [ ] `Application` ArgoCD `` = **Synced** + **Healthy**
+- [ ] Pod `Running` dans le namespace `` (SA ``)
+- [ ] Secret K8s `vso-db-credentials` présent et **roté** par VSO (TTL ~1 h) ; le pod redémarre à la rotation
+- [ ] L'app répond sur son ingress (`.arcodange.lab` ou `.arcodange.fr`)
+- [ ] Connexion DB OK via `pgbouncer.tools` avec un user dynamique héritant de `_role`
+
+## Related
+
+- [README du runbook](README.md) â vue d'ensemble + carte de bout en bout.
+- [Conventions de nommage](conventions.md) â la cohĂ©rence du nom ``, source de la plupart des ratĂ©s.
diff --git a/doc/runbooks/new-web-app/README.md b/doc/runbooks/new-web-app/README.md
new file mode 100644
index 0000000..dc2eb51
--- /dev/null
+++ b/doc/runbooks/new-web-app/README.md
@@ -0,0 +1,104 @@
+[Factory](../../../README.md) > [Doc](../../README.md) > [Runbooks](../README.md) > **Nouvelle application web**
+
+# Mettre en service une nouvelle application web
+
+> **Last Updated:** 2026-05-31
+> **Status:** â
Procédure courante
+> **Related:** [Conventions de nommage](conventions.md) · [Checklist](08-checklist.md) · [ADR CI/CD](../../adr/03_cicd_gitea_action_argocd.md) · [ADR Vault](../../adr/04_tool_hashicorp_vault.md)
+
+## C'est quoi ?
+
+Ce runbook décrit, de zéro, comment faire vivre une nouvelle application web sur la plateforme Arcodange. Le pattern est **GitOps** : l'app habite son propre dépÎt Gitea, sa base de données et ses accÚs Vault sont provisionnés par Terraform/OpenTofu, et **ArgoCD** déploie son chart Helm dans un namespace dédié. Les identifiants Postgres ne sont jamais écrits en clair : ils sont générés à la volée par Vault et injectés dans le pod par le **Vault Secrets Operator (VSO)**.
+
+La mĂ©canique est rĂ©partie sur **trois dĂ©pĂŽts** â le dĂ©pĂŽt plateforme [`factory`](https://gitea.arcodange.lab/arcodange-org/factory), le dĂ©pĂŽt des services partagĂ©s [`tools`](https://gitea.arcodange.lab/arcodange-org/tools), et le **nouveau dĂ©pĂŽt de l'app** â avec des **dĂ©pendances d'ordre** strictes (voir plus bas). Les exemples de rĂ©fĂ©rence sont [`erp`](https://gitea.arcodange.lab/arcodange-org/erp) (image publique + DB) et [`webapp`](https://gitea.arcodange.lab/arcodange-org/webapp) (image maison + DB).
+
+**Sert Ă :**
+
+1. Créer un dépÎt Gitea et son squelette (`chart/`, `iac/`, `.gitea/workflows/`).
+2. Provisionner la base de données, son rÎle propriétaire, et les accÚs Vault (statiques + dynamiques).
+3. Déployer l'app via ArgoCD et l'exposer derriÚre Traefik (interne `.lab` ou public `.fr` + CrowdSec).
+
+## Carte de bout en bout
+
+```mermaid
+%%{init: {'theme': 'base'}}%%
+flowchart TB
+ classDef step fill:#2563eb,stroke:#1e40af,color:#fff
+ classDef plat fill:#059669,stroke:#047857,color:#fff
+ classDef gitops fill:#7c3aed,stroke:#6d28d9,color:#fff
+ classDef run fill:#b45309,stroke:#92400e,color:#fff
+
+ REPO["1 · DépÎt Gitea
arcodange-org/âčappâș"]:::step
+ DB["2 · factory/postgres/iac
base âčappâș + rĂŽle âčappâș_role"]:::plat
+ VAULT["3 · tools/hashicorp-vault/iac
gitea_cicd_âčappâș + policies âčappâș / âčappâș-ops"]:::plat
+ CONTENT["4·5·6 · chart/ + iac/ + .gitea/
push â CI build image & tofu apply"]:::step
+ ARGO["7 · factory/argocd/values.yaml
â Application ArgoCD (ns âčappâș)"]:::gitops
+ POD["Runtime · Pod(SA âčappâș) â VSO â Vault
creds/âčappâș â pgbouncer.tools â base âčappâș"]:::run
+
+ REPO --> DB
+ REPO --> VAULT
+ DB --> CONTENT
+ VAULT --> CONTENT
+ CONTENT --> ARGO
+ ARGO --> POD
+```
+
+## Ordre des opérations (le point le plus important)
+
+> [!IMPORTANT]
+> Les Ă©tapes ne sont pas interchangeables. Le rĂŽle JWT de CI `gitea_cicd_` (Ă©tape 3) et le rĂŽle Postgres `_role` (Ă©tape 2) doivent **exister avant** que la CI Terraform de l'app (Ă©tape 6 appliquant l'Ă©tape 5) ne s'exĂ©cute â sinon l'authentification Vault de la CI Ă©choue, ou le module `app_roles` n'a pas de `_role` Ă qui rattacher les credentials dynamiques.
+
+```
+[01] DépÎt Gitea sous arcodange-org (hérite les secrets CI d'org)
+ â
+ âââ> [02] factory/postgres/iac â base + _role + user_lookup()
+ â
+ âââ> [03] tools/hashicorp-vault/iac â gitea_cicd_ (JWT CI) + policies / -ops
+ â (02 et 03 indĂ©pendants entre eux, mais TOUS DEUX avant 05)
+ âŒ
+[04+05+06] Contenu du dépÎt : chart/ + iac/ + .gitea/workflows/ (+ Dockerfile)
+ â push â CI « dockerimage » build l'image · CI « vault » applique iac/
+ â â creds/ (rĂŽle DB dynamique) + rĂŽle K8s + secrets KV
+ âŒ
+[07] factory/argocd/values.yaml â ArgoCD crĂ©e l'Application â dĂ©ploie le chart dans le namespace
+ âŒ
+Runtime : Pod(SA ) â VSO â VaultAuth(role ) â creds/
+ â user PG dynamique hĂ©ritant de _role â pgbouncer.tools â base
+```
+
+## Prérequis plateforme (déjà en place)
+
+Ces fondations existent et ne sont **pas** Ă refaire pour chaque app :
+
+| Brique | OĂč | RĂŽle |
+|---|---|---|
+| Mounts Vault `kvv2`, `postgres`, `transit`, auth `kubernetes` | [`tools/hashicorp-vault/iac/main.tf`](https://gitea.arcodange.lab/arcodange-org/tools/src/branch/main/hashicorp-vault/iac/main.tf) | Moteurs de secrets + auth K8s |
+| Connexion VaultâPostgres (via `pgbouncer.tools`, user `credentials_editor`) | idem | Permet Ă Vault d'Ă©mettre des users PG dynamiques |
+| RĂŽle JWT de bootstrap `gitea_cicd` + app OAuth2 Gitea (`gitea_app_id`) | [`gitea_oidc_auth.yml`](https://gitea.arcodange.lab/arcodange-org/factory/src/branch/main/ansible/arcodange/factory/playbooks/tools/roles/hashicorp_vault/tasks/gitea_oidc_auth.yml) | Ăchange OIDC Gitea â JWT Vault dans la CI |
+| Bot `tofu_module_reader` (clé SSH dans `kvv1/gitea/tofu_module_reader`) | [`factory/iac/gitea_tofu_ci_user.tf`](https://gitea.arcodange.lab/arcodange-org/factory/src/branch/main/iac/gitea_tofu_ci_user.tf) | Laisse la CI cloner le module partagé `tools` en SSH |
+| Secrets Actions d'organisation (`HOMELAB_CA_CERT`, `vault_oauth__sh_b64`, `PACKAGES_TOKEN`) | org Gitea `arcodange-org` | Hérités par tout dépÎt de l'org |
+
+## Index des étapes
+
+| # | Page | Ce qu'on y fait | Statut |
+|---|---|---|---|
+| â | [Conventions de nommage](conventions.md) | Le nom `` rĂ©utilisĂ© Ă l'identique partout (Ă lire en premier) | â
|
+| 01 | [DĂ©pĂŽt Gitea](01-gitea-repo.md) | CrĂ©er le dĂ©pĂŽt sous `arcodange-org` + squelette | â
|
+| 02 | [Base de donnĂ©es](02-database.md) | `factory/postgres/iac` â base `` + rĂŽle `_role` | â
|
+| 03 | [Vault plateforme](03-vault-platform.md) | `tools/hashicorp-vault/iac` â `gitea_cicd_` + policies | â
|
+| 04 | [Chart Helm](04-helm-chart.md) | Le chart de l'app (DB via pgbouncer, secrets VSO, ingress) | â
|
+| 05 | [Terraform de l'app](05-app-terraform.md) | `iac/` â module `app_roles` (creds dynamiques + rĂŽle K8s) | â
|
+| 06 | [Workflows CI](06-ci-workflows.md) | `.gitea/workflows/` : `tofu apply` + build image | â
|
+| 07 | [Enregistrement ArgoCD](07-argocd-register.md) | `factory/argocd/values.yaml` â Application + dĂ©ploiement | â
|
+| 08 | [Checklist](08-checklist.md) | RĂ©capitulatif ordonnĂ© + definition of done | â
|
+
+## Légende de statut
+
+â
actif · đĄ dĂ©gradĂ©/beta · đŽ critique/EOL · â ïž problĂšme connu · â dĂ©sactivĂ©
+
+## Comment éditer ce runbook
+
+1. **Ajouter une page** â la crĂ©er depuis le template tree-docs adĂ©quat **et** ajouter sa ligne dans l'index ci-dessus.
+2. **Garder les liens croisĂ©s bidirectionnels** â toute dĂ©pendance citĂ©e dans une page (`Upstream`/`Downstream`) doit avoir sa rĂ©ciproque sur l'autre page.
+3. **Mettre Ă jour `Last Updated:`** ci-dessus aprĂšs tout changement de structure.
+4. Les exemples cités (`erp`, `webapp`) sont vivants : revérifier les snippets contre le code réel avant de s'y fier aveuglément.
diff --git a/doc/runbooks/new-web-app/conventions.md b/doc/runbooks/new-web-app/conventions.md
new file mode 100644
index 0000000..4569e47
--- /dev/null
+++ b/doc/runbooks/new-web-app/conventions.md
@@ -0,0 +1,55 @@
+[Factory](../../../README.md) > [Doc](../../README.md) > [Runbooks](../README.md) > [Nouvelle application web](README.md) > **Conventions de nommage**
+
+# Conventions de nommage
+
+> **Pourquoi cette page.** Un seul nom â `` â est rĂ©utilisĂ© Ă l'identique dans une dizaine de systĂšmes (Gitea, Postgres, Vault, Kubernetes, ArgoCD, GCS, DNS). Toutes les Ă©tapes du runbook en dĂ©pendent ; on le centralise ici pour ne pas le rĂ©-expliquer partout.
+> **Audience.** Quiconque crée ou audite une app sur la plateforme.
+> **Status.** Actif · revu le 2026-05-31.
+
+---
+
+## TL;DR
+
+Choisis **un** identifiant `` en **kebab-case minuscule** (`webapp`, `erp`, `dance-lessons-coach`, `url-shortener`). Ce nom devient, **sans variation**, la clé de toutes les ressources. Une seule faute de frappe quelque part casse la chaßne (auth Vault, rattachement DB, sync ArgoCD).
+
+## Le nom `` dans chaque systĂšme
+
+| SystÚme | Identifiant dérivé de `` | Exemple (`erp`) | Source de vérité |
+|---|---|---|---|
+| DépÎt Gitea | `arcodange-org/` (ou `arcodange/` si `org` surchargé) | `arcodange-org/erp` | [argocd/templates/apps.yaml](https://gitea.arcodange.lab/arcodange-org/factory/src/branch/main/argocd/templates/apps.yaml) |
+| Base PostgreSQL | `` | `erp` | [postgres/iac/main.tf](https://gitea.arcodange.lab/arcodange-org/factory/src/branch/main/postgres/iac/main.tf) |
+| RÎle propriétaire PG (non-login) | `_role` | `erp_role` | postgres/iac/main.tf |
+| RĂŽle DB dynamique Vault | `postgres/creds/` | `postgres/creds/erp` | [modules/app_roles/main.tf](https://gitea.arcodange.lab/arcodange-org/tools/src/branch/main/hashicorp-vault/iac/modules/app_roles/main.tf) |
+| RĂŽle d'auth Kubernetes (Vault) | `` | `erp` | modules/app_roles/main.tf |
+| Policy Vault **runtime** (pod) | `` | `erp` | [modules/app_policy/main.tf](https://gitea.arcodange.lab/arcodange-org/tools/src/branch/main/hashicorp-vault/iac/modules/app_policy/main.tf) |
+| Policy Vault **CI** (ops) | `-ops` | `erp-ops` | modules/app_policy/main.tf |
+| RĂŽle JWT de CI (Vault) | `gitea_cicd_` | `gitea_cicd_erp` | modules/app_policy/main.tf |
+| Groupe d'identité Vault | `-ops` | `erp-ops` | modules/app_policy/main.tf |
+| Secret KV de config | `kvv2//config` | `kvv2/erp/config` | modules/app_roles (sortie `kvv2_path_prefix`) |
+| Namespace Kubernetes | `` | `erp` | apps.yaml (`CreateNamespace=true`) |
+| ServiceAccount Kubernetes | `` | `erp` | [chart/templates/serviceaccount.yaml](https://gitea.arcodange.lab/arcodange-org/erp/src/branch/main/chart/templates/serviceaccount.yaml) |
+| Application ArgoCD | `` | `erp` | apps.yaml |
+| Préfixe d'état OpenTofu (GCS) | `/main` | `erp/main` | [iac/backend.tf](https://gitea.arcodange.lab/arcodange-org/erp/src/branch/main/iac/backend.tf) |
+| Domaine interne | `.arcodange.lab` | `erp.arcodange.lab` | chart/values.yaml (ingress) |
+| Domaine public | `.arcodange.fr` | `webapp.arcodange.fr` | chart/values.yaml (ingress) |
+
+## Pourquoi cette uniformité est structurante
+
+Les briques se « branchent » entre elles **par convention de nom**, pas par configuration explicite :
+
+- Le module `app_roles` gĂ©nĂšre un user PG dynamique avec `GRANT _role TO âŠ` â il **suppose** que `_role` (créé Ă l'[Ă©tape 2](02-database.md)) porte exactement ce nom.
+- Le `VaultDynamicSecret` du chart lit `postgres/creds/` â il **suppose** que le rĂŽle Vault (créé Ă l'[Ă©tape 5](05-app-terraform.md)) porte exactement ``.
+- L'`Application` ArgoCD dĂ©duit `repoURL=.../`, `path=chart`, `namespace=` du seul nom â le dĂ©pĂŽt ([Ă©tape 1](01-gitea-repo.md)) et le namespace doivent matcher.
+
+â
**Utilise un nom court, stable, kebab-case** dÚs le départ.
+â **N'introduis pas** de variantes (`my_app` vs `my-app`, `MyApp`, pluriels) : rien ne te prĂ©viendra, l'app Ă©chouera silencieusement Ă se connecter ou Ă se dĂ©ployer.
+
+## Références croisées
+
+- [01 · DĂ©pĂŽt Gitea](01-gitea-repo.md) â fixe `` comme nom de dĂ©pĂŽt sous `arcodange-org`.
+- [02 · Base de donnĂ©es](02-database.md) â crĂ©e `` et `_role`.
+- [03 · Vault plateforme](03-vault-platform.md) â crĂ©e `gitea_cicd_`, policies `` / `-ops`.
+- [04 · Chart Helm](04-helm-chart.md) â rĂ©fĂ©rence ``, `creds/`, `kvv2//config`.
+- [05 · Terraform de l'app](05-app-terraform.md) â appelle `app_roles` avec `name=`.
+- [06 · Workflows CI](06-ci-workflows.md) â s'authentifie avec `gitea_cicd_`.
+- [07 · Enregistrement ArgoCD](07-argocd-register.md) â dĂ©clare `` dans `gitea_applications`.