[vibe](../../README.md) > [Guidebooks](../README.md) > **Applications** # Applications > **Status:** ✅ Active > **Last Updated:** 2026-06-23 > **Upstream:** [Lab ecosystem hub](../lab-ecosystem/README.md) · [01 · factory](../lab-ecosystem/01-factory.md) > **Downstream:** [webapp](webapp.md) · [url-shortener](url-shortener.md) > **Related:** [naming-conventions](../lab-ecosystem/naming-conventions.md) · [tools secrets-and-vso](../tools/secrets-and-vso.md) · [factory postgres-iac](../factory-provisioning/opentofu/postgres-iac.md) · [safe-prod-like-environment ADR](../../ADR/0001-safe-prod-like-environment.md) This guidebook maps the **deployed applications** — the workloads ArgoCD runs in their own `` namespace — and, more importantly, the **single repeatable pattern** every one of them follows. Once you know the pattern, every app reads as a variation on the same skeleton: a Gitea repo whose contents (Dockerfile + Helm chart + optional Vault IaC + CI) and whose `` name fully determine how it builds, deploys, gets its secrets, and is reached from the network. Two apps are presented in depth as the canonical archetypes: [webapp](webapp.md) (Go + external Postgres) and [url-shortener](url-shortener.md) (Rust + embedded SQLite). Other apps in the cluster — `erp`, `dance-lessons-coach`, `telegram-gateway`, `plausible` — instantiate the same pattern; `erp` has its own guidebook (forthcoming) and is not linked here yet. ## The common app pattern Every application is a self-contained Gitea repo under the `arcodange-org` (or `arcodange`) org that carries the same four ingredients. The `` name — the repo name — is the join key that threads through all of them (see the [naming-conventions concept](../lab-ecosystem/naming-conventions.md)). | Ingredient | Path in the app repo | What it is | Required? | |---|---|---|---| | **Dockerfile** | `Dockerfile` | Multi-stage build producing the runtime image, pushed to the Gitea container registry as `gitea.arcodange.lab//` | ✅ always | | **Helm chart** | `chart/` | `Chart.yaml` + `values.yaml` + `templates/` (deployment, service, ingress, serviceaccount, hpa, config, NOTES, optional PVC, optional Vault CRDs) — the unit ArgoCD syncs | ✅ always | | **Vault IaC** | `iac/` | OpenTofu that declares the app's Vault objects: a Postgres dynamic-secret role keyed on `` + a Kubernetes auth role bound to the `` ServiceAccount. The canonical form pulls the [`app_roles` module](../tools/secrets-and-vso.md) from `tools`; the privileged `app_policy` half is declared centrally so the app repo never holds it | 🟡 only apps needing Postgres / Vault KV | | **CI workflows** | `.gitea/workflows/` | A `dockerimage` job that builds + pushes the image on every `main` push, and (when `iac/` exists) a `vault` job that runs `tofu apply` against Vault, gated to changes under `iac/*.tf` | ✅ image build · 🟡 vault apply | ### How a chart becomes a running app Factory's [ArgoCD app-of-apps](../lab-ecosystem/01-factory.md) emits **one `Application` CRD per app**, and every field is derived mechanically from the `` name: | Application field | Value | Source | |---|---|---| | `repoURL` | `https://gitea.arcodange.lab//` | `` + optional org override | | `path` | `chart` | fixed convention | | `namespace` | `` (`CreateNamespace=true`) | `` | | `syncPolicy` | `automated` with `prune: true` + `selfHeal: true` | app-of-apps default | The same `` name is also the Postgres database/role name, the Vault role name, the KV path prefix, and the ServiceAccount name — one string keying the whole stack. See [naming-conventions](../lab-ecosystem/naming-conventions.md). ### Ingress convention — `.fr` public vs `.lab` internal Every app that serves HTTP exposes itself through two Traefik ingresses with a fixed split by domain suffix: | Ingress | Domain | Traefik entrypoint | Middlewares | TLS / cert | Reached via | |---|---|---|---|---|---| | **Public** | `.arcodange.fr` | `web` | `kube-system-crowdsec@kubernetescrd` (CrowdSec bouncer) | terminated at the edge | the **Cloudflared tunnel** — the public web entrypoint | | **Internal** | `.arcodange.lab` | `websecure` | `localIp@file` (LAN-only allow-list) | a cert from **either** the Traefik `letsencrypt` resolver **or** cert-manager's `step-issuer` (`StepClusterIssuer`) | the LAN directly | > [!NOTE] > The two archetypes differ only in cert mechanism, not in the convention: webapp's internal ingress carries the `letsencrypt` certresolver annotations, while url-shortener's internal ingress requests its cert from cert-manager's `step-issuer`. Both still ride `websecure` + `localIp@file`; both still expose a `.fr` twin behind the CrowdSec middleware. ## Two archetypes compared The deployed apps fall into two shapes. Pick the matching archetype's page when adding or modifying an app. | Aspect | [webapp](webapp.md) | [url-shortener](url-shortener.md) | |---|---|---| | Language / build | **Go** (golang:1.23 → alpine runtime) | **Rust** (cargo-chef → `scratch` runtime) | | State | **External Postgres**, reached through the `tools` **pgbouncer** pooler with credentials delivered by **VSO** | **Embedded SQLite** on a `/data` file | | Persistence | none in-cluster (DB lives on `pi2`) | a **Longhorn RWO PVC** (`storageClassName: longhorn`, `helm.sh/resource-policy: keep`) mounted at `/data` | | Replicas | **scalable** (stateless pods; HPA-ready) | **single** — RWO volume cannot be shared across pods | | `iac/` + Vault | **yes** — declares a Postgres dynamic-secret role + a k8s auth role; pod consumes **dynamic, rotating** DB creds via `VaultAuth` + `VaultDynamicSecret` + `VaultStaticSecret` CRDs | **none** — no Vault objects, no DB role | | Recovery | restore from the **PostgreSQL backup** (factory `05_backup` → `/mnt/backups`) | **Longhorn block-device recovery** of the PVC (raw replica `.img` files) — see [ansible recover](../factory-provisioning/ansible/06-recover.md) | The choice is essentially *"shared/scalable state that survives a single node"* (Postgres, webapp shape) versus *"self-contained single-writer state co-located with the pod"* (SQLite-on-Longhorn, url-shortener shape). The trade-off and why both are kept prod-like is recorded in the [safe-prod-like-environment ADR](../../ADR/0001-safe-prod-like-environment.md). ## Generic app lifecycle ```mermaid %%{init: {'theme': 'base'}}%% flowchart LR classDef src fill:#2563eb,stroke:#1e40af,color:#fff classDef proc fill:#059669,stroke:#047857,color:#fff classDef store fill:#7c3aed,stroke:#6d28d9,color:#fff classDef net fill:#b45309,stroke:#92400e,color:#fff REPO["app repo
Dockerfile + chart/ + iac/ + .gitea/workflows"]:::src IMG["image pushed
gitea registry <org>/<app>"]:::store VAULT["tofu apply
Vault role + Postgres role"]:::store ARGO["ArgoCD
deploys chart (ns <app>)"]:::proc POD["pod
Postgres via pgbouncer + VSO
OR SQLite on Longhorn PVC"]:::proc TR["Traefik ingress
.fr public / .lab internal"]:::net REPO -- "dockerimage CI" --> IMG REPO -- "vault CI (iac apps only)" --> VAULT IMG --> ARGO VAULT -. "creds for DB apps" .- POD ARGO --> POD POD --> TR ``` 1. The **app repo** holds the four ingredients: a Dockerfile, a `chart/`, an optional `iac/`, and `.gitea/workflows`. 2. On a push to `main`, the **dockerimage** workflow builds the image and pushes it to the Gitea container registry as `/`. 3. For apps with an `iac/`, the **vault** workflow runs `tofu apply` to declare the app's Postgres dynamic-secret role and Kubernetes auth role in Vault. 4. **ArgoCD** (factory's app-of-apps) syncs the chart into the `` namespace, deriving `repoURL`/`path`/`namespace` from the `` name. 5. The **pod** comes up; a Postgres-backed app receives rotating DB credentials through pgbouncer + VSO, while a SQLite-backed app mounts its Longhorn PVC at `/data`. 6. **Traefik** publishes the pod through two ingresses: the `.fr` public route (CrowdSec middleware, via the Cloudflared tunnel) and the `.lab` internal route (`websecure` + `localIp` + a letsencrypt/step-issuer cert). ## Index | Page | Archetype | Status | |---|---|---| | [webapp](webapp.md) | Canonical **Go + external Postgres** exemplar — `iac/` + Vault dynamic creds, scalable stateless pods | ✅ Active | | [url-shortener](url-shortener.md) | **Rust + embedded SQLite** counterpart — single replica on a Longhorn RWO PVC, no Vault | ✅ Active | `erp` and the other apps (`dance-lessons-coach`, `telegram-gateway`, `plausible`) follow the same pattern; `erp` will be cross-linked here once its dedicated guidebook ships. ## Maintenance rule > [!IMPORTANT] > **When an app's repo changes shape, its page here changes in the same PR.** If you alter the chart structure, the ingress convention, the Vault wiring, the persistence model, or the CI workflows of a deployed app, update this hub and the relevant archetype page in the same change. A reference map that drifts from the real `chart/` and `iac/` sends agents confidently down dead paths. ## Cross-references - [01 · factory](../lab-ecosystem/01-factory.md) — the ArgoCD app-of-apps that emits one `Application` per app on this page. - [tools secrets-and-vso](../tools/secrets-and-vso.md) — the `app_policy` + `app_roles` module pair that turns `` into Vault policies, roles, and CI identities; the VSO runtime path the Postgres archetype rides. - [factory postgres-iac](../factory-provisioning/opentofu/postgres-iac.md) — the per-app PostgreSQL database + `_role` the webapp archetype depends on. - [naming-conventions](../lab-ecosystem/naming-conventions.md) — the `` join key that threads through repo, image, namespace, DB, Vault role, and ServiceAccount. - [safe-prod-like-environment ADR](../../ADR/0001-safe-prod-like-environment.md) — why the lab keeps apps deployed prod-like and the state/recovery trade-offs behind the two archetypes.