Example: the Penvoice API
The generic recipe is in Onboard an app. This page is Penvoice filled in on this Project Loom node. Ayde, Z1, or the next API use the same folders with different names.
flowchart LR Push([merge app main]) --> CI[CI → GHCR sha] CI --> Stg[overlays/staging newTag] Stg --> Argo[Argo CD] Promo[Promote] --> Prod[overlays/prod same digest] Inf[Infisical GUI] -->|operator| Sec[(penvoice-api-kc)] Sec --> RO[Rollout] Argo --> RO
Staging host test.api.penvoice.app, prod api.penvoice.app. Namespaces penvoice-staging / penvoice-prod.
Distroless static arm64, tag ghcr.io/webb1es/penvoice-api:sha-<short>. Schema applies in-process at startup.
Manifests
Section titled “Manifests”ApplicationSet rows in apps/workloads.yaml. Files under workloads/penvoice/base/ plus overlay patches (namespace,
host, Infisical slug, image tag, analysis not present).
Infisical project penvoice, cells staging and prod. Git has projectId + slug only. Not in Infisical: DB_URL
(CNPG uri), container port.
Local dev
Section titled “Local dev”Same Infisical project, a third cell: dev. penvoice-api’s .infisical.json points at the same projectId as the
overlays, defaultEnvironment: dev; infisical run -- go run . (or infisical run -- air) replaces .env. DB_URL
and API_PORT are dev-only (no CNPG, no shared manifest locally); KC_ISSUER reuses the staging value (same UAT
Keycloak); SWAGGER_SESSION_SECRET is its own value, not shared with staging/prod. Recipe:
Onboard → Secrets § Wire local dev.
Canary
Section titled “Canary”Prod: 25→50→75, 60s pauses. Staging: one replica, weight 100. The API may still export /metrics for a future
scrape stack. Nothing in Git scrapes it today.