Files
factory/vibe/guidebooks/applications/url-shortener.md
Gabriel Radureau 4823394e0e docs(vibe): add applications/ guidebook (webapp + url-shortener)
Tree-docs guidebook under vibe/guidebooks/applications/ documenting the common
app pattern and two contrasting archetypes, drilling into lab-ecosystem/01-factory
(bidirectional):

- README.md  : the shared app pattern (repo = Dockerfile + chart + optional iac +
  CI; ArgoCD app-of-apps; the <app> join key; .fr vs .lab ingress conventions) +
  a two-archetype comparison.
- webapp.md  : canonical Go + Postgres exemplar (chart, VaultAuth/Static/Dynamic
  CRDs, inline iac vs the shared app_roles module, CI); notes the current nuance
  that the live pod still uses the static pgbouncer_auth DATABASE_URL.
- url-shortener.md : Rust + SQLite-on-Longhorn-RWO counterpart (single replica,
  no iac/no Vault, CI mirrors the upstream image); the power-cut recovery story.

erp is referenced in prose only (its own guidebook lands next). Sibling-repo code
via full gitea URLs; 2 mermaid diagrams MCP-validated; zero dead links.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 21:58:36 +02:00

14 KiB

vibe > Guidebooks > Applications > url-shortener

url-shortener (Chhoto URL)

Status: active · Last Updated: 2026-06-23 Upstream: Applications index · lab-ecosystem hub Downstream: storage-and-recovery · Ansible recover playbooks Related: webapp (the stateless counterpart) · naming-conventions · secrets-and-vault

url-shortener is the lab's stateful application — the deliberate mirror image of webapp. Where webapp is a horizontally-scalable, Postgres-backed, Vault-credentialed reference app, url-shortener is a single-pod, SQLite-on-a-disk service that proves the lab can run a genuinely stateful workload on Longhorn block storage and recover it after a crash.

The application itself is Chhoto URL, a tiny Rust/actix-web URL shortener, mirrored into the lab from the upstream project sintan1729/chhoto-url. The lab does not fork the source — it mirrors the published Docker image (see §5 CI) — but keeps a copy of the source and a custom Helm chart in the url-shortener repo so the lab owns its packaging.

Note

A second public web app, the erp application, exists in the ecosystem but does not yet have a guidebook page; it is mentioned here in prose only and will be cross-linked once its guidebook ships.


1. The app & image

Chhoto URL is a single self-contained binary: an actix-web HTTP server with a bundled SQLite engine and a plain static frontend. There is no separate database process and no application runtime to install.

Aspect Detail Source
Language / framework Rust, actix-web 4.5.x actix/Cargo.toml
Database driver rusqlite with the bundled feature — SQLite is compiled into the binary Cargo.toml
Sessions actix-session cookie store; the session key is regenerated at boot, so a restart invalidates all logins actix/src/main.rs
Frontend Plain HTML/CSS/JS served by actix-files from resources/ (index.html, static/styles.css, static/script.js, static/404.html) — no build step, no SPA framework resources/
Listen port 4567 (overridable via the port env var) actix/src/main.rs
Slug generation Adjective-name Pair or UID styles; the lab forces UID (see §3) actix/src/utils.rs

Image build

Image Base / approach Result File
Dockerfile cargo-chef dependency caching → musl static build (x86_64-unknown-linux-musl) → FROM scratch, copying only the binary + resources/ A single-arch, ~6 MB image with no OS, shell, or libc Dockerfile
Dockerfile.multiarch Per-arch FROM scratch stages selected by TARGETARCH, copying pre-built musl binaries amd64 / arm64 / armv7 images from a local cross-compile Dockerfile.multiarch
compose.yaml Pulls the upstream sintan1729/chhoto-url:latest, binds 4567, mounts a db volume at the SQLite path Local-dev only — not the deployed topology compose.yaml

The FROM scratch design is what makes the storage story so stark: the container has nothing except the SQLite file on its mounted volume. All durable state is one file.


2. Storage — the key contrast

This is the section that distinguishes url-shortener from every other lab app. The entire database is a single SQLite file, /data/urls.sqlite, living on a Longhorn PersistentVolumeClaim.

PVC field Value Why it matters
accessModes ReadWriteOnce (RWO) Only one node can mount the volume at a time
resources.requests.storage 128Mi A URL table is tiny; this is generous
storageClassName longhorn Replicated block storage; the source of durability
annotations helm.sh/resource-policy: keep The PVC (and its data) survives helm uninstall
Mount /data → SQLite at /data/urls.sqlite Set by the ConfigMap db_url (§3)

Source: chart/templates/pvc.yaml and the volume mount in chart/templates/deployment.yaml.

Because the volume is RWO, replicaCount is hard-pinned to 1. A second pod cannot mount the same volume, so there is no HA and no rolling update — the next pod can only start after the previous one has released the disk. This is the exact data shape that was reconstructed during the 2026-04-13 power-cut incident via Longhorn block-device recovery (see §6).


3. The chart

url-shortener ships its own Helm chart at chart/. It is deployed by ArgoCD like every other lab app, following the naming conventions.

Concern Setting Source
Image gitea.arcodange.lab/arcodange-org/url-shortener, tag defaults to .Chart.AppVersion (6.5.3) values.yaml, Chart.yaml
Service ClusterIP, port 4567 service.yaml
Internal ingress Host url.arcodange.lab on websecure; TLS via cert-manager StepClusterIssuer (step-issuer); localIp@file middleware (LAN-only) values.yaml
Public ingress Host derived from the internal host by the .lab → .fr substitution in _helpers.tpl; PathRegexp matcher (/[^/]+) so it intercepts shortlink redirects; no localIp (it carries the crowdsec middleware instead, since the public path must be reachable) public-ingress.yaml, _helpers.tpl
ConfigMap db_url=/data/urls.sqlite, site_url = the .fr FQDN, slug_style=UID, slug_length=4 config.yaml
Probes Liveness + readiness HTTP GET / on the http port values.yaml
Autoscaling Disabled. maxReplicas > 1 would fail under RWO (a second pod cannot mount the volume) hpa.yaml, values.yaml

Note

The _helpers.tpl url-shortener.fqdn template builds the public site_url by taking the first internal ingress host and running replace ".lab" ".fr" — so the same value drives the internal url.arcodange.lab and the public .fr redirect domain. This is the same .lab ↔ .fr split documented in naming-conventions.


4. No iac/, no Vault — a deliberate deviation

url-shortener has no iac/ directory and no Vault CRDs, and this is intentional, not an oversight.

Convention (see webapp) Why url-shortener skips it
iac/ OpenTofu module declaring a Postgres role There is no Postgres. SQLite is embedded; there is no database server to provision.
Vault app_roles + VSO-synced dynamic credentials There are no credentials to issue — the app talks to a local file, not a networked database. See secrets-and-vault for the pattern url-shortener opts out of.

For a SQLite-on-a-file app, the Helm chart is the IaC: the PVC, the ConfigMap, and the ingress fully describe the deployable surface. There is no second provisioning tier. Compare with webapp, where the Postgres role and Vault role are first-class infrastructure objects managed outside the chart.


5. CI — mirror, not build

The single workflow, .gitea/workflows/dockerimage.yaml, does not build the local actix/ source. It mirrors the upstream image.

Step Action
1. Discover version wget the upstream Cargo.toml from SinTan1729/chhoto-url on GitHub and parse version
2. Login Authenticate to gitea.arcodange.lab using the PACKAGES_TOKEN secret
3. Pull docker pull sintan1729/chhoto-url:<tag> from Docker Hub (latest and the discovered version)
4. Retag docker tag to gitea.arcodange.lab/<repo>:<tag>
5. Push docker push both latest and the version tag to the Gitea registry

So the in-repo actix/ source and the Dockerfile/Dockerfile.multiarch exist for reference and local cross-compilation (Makefile targets build-release / docker-release use cross for amd64 / arm64 / armv7), but the cluster runs the mirrored upstream image. The multi-arch build is a manual, local-developer flow — it is not run in CI.


6. Recovery

url-shortener's SQLite-on-RWO is the canonical example that the lab's recovery tooling targets. When a node dies mid-write or the cluster loses power, the durable artifact to reconstruct is exactly this kind of Longhorn block volume.

  • The concept and policy live in storage-and-recovery.
  • The mechanics live in the Ansible recover playbooks: the longhorn_data.yml block-device recovery flow reconstructs precisely this single-file SQLite volume, which is what was recovered in the 2026-04-13 power-cut.

Warning

Single replica + RWO means downtime on any pod move: a node drain, an upgrade, or a reschedule cannot overlap the old and new pods — the new one waits for the disk to detach. There is no application-level redundancy. The only durability is Longhorn volume replication plus backups; if the volume is lost and unbacked, the URL database is gone. Treat backup health as the single point of failure for this app.


7. Deviations from convention (vs webapp)

A side-by-side of where url-shortener (stateful) departs from webapp (the stateless reference):

Dimension webapp url-shortener
Database PostgreSQL (external server) SQLite (embedded, single file)
Vault / secrets app_roles + VSO-synced dynamic creds None — no networked credentials
iac/ directory Yes (Postgres role, Vault role) None — the Helm chart is the IaC
Replicas Scalable (HPA-eligible) 1, hard-pinned (RWO forbids more)
Rolling update / HA Yes No — single pod, downtime on move
CI Builds the app source Mirrors the upstream image
Recovery shape Postgres backup/restore Longhorn block-device recovery

8. Deploy + storage path

%%{init: {'theme':'base'}}%%
flowchart LR
    upstream["Docker Hub<br>sintan1729/chhoto-url"]
    ci["Gitea Actions<br>dockerimage.yaml (mirror)"]
    reg["Gitea registry<br>gitea.arcodange.lab/<br>arcodange-org/url-shortener"]
    argo["ArgoCD + Helm chart"]
    pod["Pod (replicaCount 1)<br>actix on :4567"]
    pvc["Longhorn PVC (RWO, 128Mi)<br>keep policy"]
    db["/data/urls.sqlite"]

    upstream -- "pull + retag" --> ci
    ci -- "push latest + version" --> reg
    reg -- "image ref" --> argo
    argo -- "deploy" --> pod
    pod -- "mounts (RWO)" --> pvc
    pvc -- "holds" --> db

    classDef box fill:#1f2933,stroke:#7b8794,color:#f5f7fa;
    class upstream,ci,reg,argo,pod,pvc,db box;
  1. Upstream — the canonical sintan1729/chhoto-url image is published to Docker Hub by the original maintainer.
  2. CI mirror — the lab's dockerimage.yaml workflow pulls that image, retags it, and pushes it to the Gitea registry (it does not build from actix/).
  3. Gitea registrygitea.arcodange.lab/arcodange-org/url-shortener holds both latest and the version tag.
  4. ArgoCD + Helm — the chart references the registry image (tag defaults to appVersion) and renders the Deployment, Service, ingresses, ConfigMap, and PVC.
  5. Pod — a single actix pod listens on 4567; HPA and rolling updates are off.
  6. Longhorn PVC — the pod mounts the RWO volume at /data; only one pod can hold it.
  7. SQLite file — all durable state is the single /data/urls.sqlite file, which is what Longhorn block-device recovery reconstructs.

See also: webapp (the stateless, Postgres-backed contrast) · Applications index · naming-conventions · storage-and-recovery.