# NEW-APP CHECKLIST — what a new catalog app must have measured before it reaches the live catalog > Operator request 2026-10-01: before new apps are added, a checklist every new app passes. Reviewer's draft the same > day, reviewed and piloted on wger by CC (`felhom.eu/documentation/audits/new-app-checklist-2026-10-01/`). > **The gate:** `scripts/check-onboarding.py` (in `catalog_gates.py --fast`, so the pre-push hook and CI run it). **What this is.** Every check below exists because something went wrong on a real app — the *why* column names it. The filled-in copy per app is the app's **onboarding record**, `onboarding/.md`, started by copying `onboarding/_TEMPLATE.md`. A new template directory without a complete record is refused by the gate. **How an item is answered.** One line per id in the record: ` | done | `, ` | n/a | ` or ` | open | `. - **done** names evidence that exists: a non-empty file, or a directory holding one. Paths are written from the workspace root — `app-catalog-felhom.eu/…` or `felhom.eu/documentation/audits/…`. Several paths: separate them with ` ; `. A note may follow the path(s) after ` — `. - **n/a** carries a reason of at least four words. - **open** blocks publishing. **Measured, not read.** "Measured" means on the bench (LXC 9401 on demo-hp) or on scratch guest 9202 (drill catalog only, `09` §6.5) through the product. Upstream reading may PLAN a test; it never CLOSES an item. Where the *how* says "read", reading is the test (a licence, a tag list). **The `since` column.** The date an id joined the list. A record carries every id whose `since` is on or before the record's `opened:` date; an id added later binds only apps opened after it. A record may answer a newer id anyway. **The 53 apps in the catalog on 2026-10-01 are exempt** (operator default, may be reversed); what the catalog already shows for them is `onboarding/EXISTING-APPS-GAPS.md`. An exempt app's record, if one exists, must still be well-formed, and may say `open`. --- ## 0. Fit — should we offer it at all? | id | since | Check | How | Why (what went wrong before) | |---|---|---|---|---| | 0.1 | 2026-10-01 | Open-source licence; we pull the upstream image, never redistribute it | read the repo's licence file | the business sells installation, not software | | 0.2 | 2026-10-01 | Upstream is alive: a release in the last 6 months, issues answered | read the repo | plant-it is `lifecycle: abandoned` (`templates/plant-it/.felhom.yml`) | | 0.3 | 2026-10-01 | An official image with **version tags** (not `latest` only), amd64 (+ arm64 if `pi_compatible`) | `docker manifest inspect :` — the tag list can be stale (REUSE.md §2 "Image pinning") | pins, digests and the ladder need real tags | | 0.4 | 2026-10-01 | **Telemetry / phone-home** — on by default? a switch to turn it off? Start-time downloads (exercise sync, model fetch)? | read the entrypoint (1.7) for start-time network jobs; on 9202, the app's log of its first start | data sovereignty is the pitch | | 0.5 | 2026-10-01 | Needs the internet at runtime (claim tokens, first-start downloads)? | docs + the first start on 9202 | plex (`PLEX_CLAIM`), adventurelog's world data (`09` decision 41), immich's geodata import (R-732) | | 0.6 | 2026-10-01 | Needs ports other than HTTP(S)? The tunnel carries HTTP only — say what the household loses | compose + docs | gitea SSH, DLNA, game servers | | 0.7 | 2026-10-01 | Phone / desktop apps: which login route do they call, and does it work through the tunnel and the setup gate? | docs for the route; then that route with `curl` through traefik on 9202 (3.9) — a real client only if one is at hand | wger's phone-app login route answered 500 while the web login worked (R-737); a gated app is unreachable for a phone app (`09` decision 46) | | 0.8 | 2026-10-01 | What a household gets from it, in one sentence (Hungarian) | — | the catalog card and `use_cases` | | 0.9 | 2026-10-01 | The app does not call ITSELF at its public name (server-side), or the bench override is recorded | the env/config the server reads for its own URL; then the bench start | wanderer cannot run on the bench at all (R-739) — no bench, no update proof | ## 1. Images, the start command and the database | id | since | Check | How | Why | |---|---|---|---|---| | 1.1 | 2026-10-01 | Every image pinned to a concrete tag that resolves | **gates `image-pins` + `image-resolvable`** (`catalog_gates.py `) | `:latest` breaks restore fidelity | | 1.2 | 2026-10-01 | Database engine and major = what the app's own upstream compose runs | upstream compose at the pinned tag | `09` decisions 37/42 (one conversion, not two) | | 1.3 | 2026-10-01 | MariaDB sidecar: `MARIADB_AUTO_UPGRADE=1`; PostgreSQL 18: mount at `/var/lib/postgresql` | compose (no gate checks either today) | R-459; PG 18 refuses `/var/lib/postgresql/data` (CLAUDE.md engine rule) | | 1.4 | 2026-10-01 | The app migrates its own database at start, **or** the switch that makes it do so is set | 1.7's read names the switch; then **prove it**: install the PREVIOUS upstream release on the bench, seed, move to the pin (`upgrade-test.py --move`), read back through the login | wger ran no migration without `DJANGO_PERFORM_MIGRATIONS`; the update said `done`, the login answered 500 (R-738). A new app at its newest tag has no "next" update to try — the step INTO the pin is the test | | 1.5 | 2026-10-01 | The app runs its production server, not a development server | process list in the running container (`ps` / `/proc/*/cmdline`) | wger ran `manage.py runserver` (R-755) | | 1.6 | 2026-10-01 | Every key and secret the app READS is set or generated — none left at an image default or empty | list the env names the app's settings read (grep its settings source for env reads of `*KEY*`, `*SECRET*`, `*TOKEN*`, `*PEM*`); each one set in the compose; then 3.9's logins on every route | wger read `JWT_PRIVATE_KEY`, the template set none: the API login answered 500 on the right password while the web login worked (R-737); grafana falls back to `admin` on an empty field (R-708) | | 1.7 | 2026-10-01 | **Every start-time switch in the image's entrypoint is read and decided** (migrations, production server, debug, start-time downloads, static files) | read the entrypoint inside the image (`docker run --rm --entrypoint cat `); list each `if $VAR` and the value the template sets | one read of wger's `entrypoint.sh` shows `DJANGO_PERFORM_MIGRATIONS` AND `WGER_USE_GUNICORN` — R-738 and R-755 in one file (`felhom.eu/documentation/audits/more-night-apps-2026-09-30/box/wger/entrypoint-read.txt`) | | 1.8 | 2026-10-01 | Debug / development mode is OFF on the public origin | 1.7's read + the running container's env; an error page through traefik shows no stack trace | adventurelog ran Django with `DEBUG=True` on the public origin (R-482) | | 1.9 | 2026-10-01 | A secret whose loss destroys data or locks people out (it encrypts stored data, or signs 2FA secrets / long-lived tokens) carries `data_key: true` and a comment saying why it must never be regenerated; a key that only signs sessions does not | what each generated secret is used for (the app's settings source) | a restore must RECOVER that key; regenerating it destroys data or locks out 2FA (felhom-app-catalog skill §4) | ## 2. Storage and backup | id | since | Check | How | Why | |---|---|---|---|---| | 2.1 | 2026-10-01 | Every path the app writes is mounted — **measured** | **gate `volume-persistence`** (`check-volume-persistence.py `, scratch host) | papra backed up an empty folder (R-156) | | 2.2 | 2026-10-01 | Where each path lives: named volume (NVMe) / `${HDD_PATH}/appdata` / `${USERDATA_PATH}` | compose; REUSE.md §2 "Compose file skeleton" | the household can browse userdata, not appdata | | 2.3 | 2026-10-01 | Backup class for every HDD path (`backup:` in `.felhom.yml`) and what the tier-1 unit holds | `.felhom.yml backup:`; the app's backup page on 9202 | DB rows point at files (07); the tier-1 unit holds NO drive-side data (R-537, R-538) | | 2.4 | 2026-10-01 | File owner / uid fits the box (PUID/PGID where the image wants them) | first start on 9202 | linuxserver images chown their tree | | 2.5 | 2026-10-01 | **A backup and a restore through the product, data read back** | 9202: seed → backup → remove → restore → read back | the promise is the restore, not the backup | | 2.6 | 2026-10-01 | "Remove with data" and "remove, keep data" both do what they say | 9202, then list what is left on the drive | "remove with data" was inert (R-442); refused with the folder present (R-756, cause not yet known) | | 2.7 | 2026-10-01 | Off-site size for a typical household | measured size after the seed + one sentence | `09` decision 50 (the page names the largest apps) | | 2.8 | 2026-10-01 | A file the household uploads opens again **through the front door** | 9202: upload through traefik, open it the way the page does | adventurelog's photos uploaded and rendered broken (R-483) | ## 3. Accounts and strangers | id | since | Check | How | Why | |---|---|---|---|---| | 3.1 | 2026-10-01 | First-admin class (FIRST-ADMIN.md 1–6), **measured**, and the household can make its first account on a fresh install | fresh install on 9202, through traefik | `09` decision 45; wishlist could not be signed up to while the deploy said success (R-612) | | 3.2 | 2026-10-01 | Known default login → `after_install` with a generated password (and a generated name, if the name is public and a lock targets it) | 9202: default fails, generated works, wrong fails | bookstack, claper (R-702), calibre-web (`09` decisions 45, 61) | | 3.3 | 2026-10-01 | Open first-run screen → `setup_gate` (+ a probe that **flips**, measured before and after) | 9202 as a stranger; **gate `probe-measured`** refuses a probe with no measurement above it | 33 apps gated today (`09` decision 46); a probe that never flips blocks the household (R-715) | | 3.4 | 2026-10-01 | Open sign-up after the setup → `signup_block` / `after_setup`; the block case-insensitive, the API sign-up blocked too | 9202 as a stranger, after the setup | R-711, R-512; `09` decisions 47–49 | | 3.5 | 2026-10-01 | The install window: a stranger reaches nothing before the password is replaced | poll the default login once a second from the install press | R-741 (fixed box-wide in controller v0.284.x — a new `after_install` app still proves it) | | 3.6 | 2026-10-01 | **Lock-out**: N wrong passwords by a stranger for the public name — who is locked (name / address / everyone), for how long | 9202 through traefik; then the household's right password, and a SECOND member's | mealie 24 h (R-747); behind the tunnel every visitor has ONE address, so a per-address lock locks everyone (R-753) — wger (R-752) | | 3.7 | 2026-10-01 | The household can change its password and add family members; the page says how | 9202 | `add_people` copy | | 3.8 | 2026-10-01 | Secrets pass to commands as arguments, never inside program code | review `after_install` | claper pasted the password into Elixir code (R-713); security review 2026-09-29 | | 3.9 | 2026-10-01 | **Sign in the way each client does, through traefik over https**: the browser form (with its https `Origin` and CSRF cookie) AND every API login route a phone/desktop app uses — right password works, wrong refused | 9202, `curl` with the headers a browser sends; the API route from 0.7 | wger refused every browser sign-in behind traefik (CSRF, R-712); wger's API login 500 (R-737) | | 3.10 | 2026-10-02 | **The visitor's address**: how the app reads it (from the RIGHT with a trusted-proxy list / a fixed count / the LEFTMOST `X-Forwarded-For` / not at all) and what it decides with it (log, lock, "local network" rights). A leftmost reader whose address decides anything carries the router reset `-xff`; a right-walking reader with a setting gets `172.16.0.0/12` trusted | read the app's client-address function at the pinned tag; then on 9202 a forged leftmost `X-Forwarded-For` through the SIMULATED tunnel (a container at `172.16.253.2`, Part A's method) must not become the address the app logs or locks | since controller v0.286.0 traefik keeps the tunnel's chain, whose leftmost entry a stranger writes (R-753); 19 catalog apps read the leftmost (`felhom.eu/documentation/audits/visitors-2026-10-01/A/sweep/`) | ## 4. Health | id | since | Check | How | Why | |---|---|---|---|---| | 4.1 | 2026-10-01 | Compose healthcheck of the family the IMAGE has (inspected, one tool per run), dialling `127.0.0.1` not `localhost` | the inspection loop in the felhom-app-catalog skill §2; REUSE.md §2 (no gate checks `localhost`; 0 templates use it today) | rallly's guessed `wget` (ENOENT); vaultwarden's IPv6 trap | | 4.2 | 2026-10-01 | The controller probe dials what the compose healthcheck dials; a real health path where one exists | **gate `probe-matches-compose`** | a wrong probe stops a working app after an update (R-618) | | 4.3 | 2026-10-01 | Healthy within `start_period` on a **cold first start** (incl. one-time imports), with no restarts | 9202, timed, `RestartCount` read | glance crash-looped on every fresh install (R-473); immich's first start restarted 12× (R-676) — `09` decision 28 stops ≥ 6 in 10 min | | 4.4 | 2026-10-01 | Negative control: a broken app reads unhealthy | stop the DB (or break the app's data dir), read the status | a probe that is always green proves nothing; uptime-kuma parked on its wizard read healthy (R-613) | | 4.5 | 2026-10-01 | The probe-named container is the stack name; sidecars `-db` … | compose (REUSE.md §2 "Probe-container naming") | `findProbeContainer` falls back to the DB; paperless's probe never ran (R-630) | ## 5. Resources | id | since | Check | How | Why | |---|---|---|---|---| | 5.1 | 2026-10-01 | **First start from birth, swap OFF**: peak `anon`, `oom_kill` = 0 | bench, the container's own cgroup sampled every 2 s from creation (`memory.stat` anon, `memory.events` oom_kill — never Docker's `OOMKilled`, R-528) | immich's DB killed at 512 MiB, hidden by swap on 9202 (R-732, R-733); calcom could not start at its limit (R-703) | | 5.2 | 2026-10-01 | 10-minute soak under the household's heaviest ordinary act: peak `anon` < 80 % of the limit, 0 kills, 0 restarts | harness memory watch (`upgrade-test.py`) + one burst of that act | romm OOM-looped for six hours after an update called success (R-635, `09` decision 22); paperless lost 11 of 20 uploads at once (R-514) | | 5.3 | 2026-10-01 | `mem_limit` in `.felhom.yml` = the sum of the compose limits, and the header comment says the same | arithmetic by hand — **no gate checks it; 8 templates differ today (R-758)** | immich's `mem_limit` was 128 MB under its sum, its header named a 256M database running at 512M (fixed `56c4888`) | | 5.4 | 2026-10-01 | Node/Java apps: does the heap size itself from the limit? | two watches at two limits | R-693 (docmost) | | 5.5 | 2026-10-01 | Pi-compatible (yes/no) and the disk the images pull | registry (image size) | `pi_compatible` is a card field; a box's Docker disk refuses installs when full (R-736) | ## 6. Updates | id | since | Check | How | Why | |---|---|---|---|---| | 6.1 | 2026-10-01 | An upgrade fixture: seed + read-back **through the app's own front door**, negative control — or the reason none can exist | `upgrade_fixtures*.py` | the box's health check sees only the front page; only the read-back saw wger broken (R-738); three apps cannot be seeded headless (R-624) | | 6.2 | 2026-10-01 | A first ladder step proven on bench + 9202 (`--write-ladder`), **or** "manual only" with the reason | harness; **gates `test-record` + `test-record-move`** check the entry once written | 38 of 53 apps carry a ladder today; zipline needed two steps where one failed (R-742) | | 6.3 | 2026-10-01 | The undo works on a real failure (one forced-fail case) | 9202, the drill catalog's image store (`09` §6.5) | zipline's real failed jump, undone in 20 s (R-742) | | 6.4 | 2026-10-01 | `files_may_change` understood (which files change at start) | bench (`files_changed_detail`) | immich's six marker files (R-734) | | 6.5 | 2026-10-01 | Tag shape is stable upstream (no `v` dropped, no flavour prefix) and whether the publisher re-pushes tags | tag list over a year; linuxserver rebuilds weekly | gramps-web, jellyfin, kimai (R-731); same-tag re-pushes (R-743, `09` decisions 52/55) | ## 7. Mail | id | since | Check | How | Why | |---|---|---|---|---| | 7.1 | 2026-10-01 | Sends mail? → `smtp_mapping` + `${VAR:-}` compose lines; a fresh install with mail OFF boots | 9202 | vaultwarden's empty-vars trap (REUSE.md §2 "App-email") | ## 8. Household-facing text and listing | id | since | Check | How | Why | |---|---|---|---|---| | 8.1 | 2026-10-01 | Hungarian + English, informal „te", no „kérjük"; parity green; freeze updated | **gate `copy-i18n`** (`check-copy-i18n.py --capture-freeze` after a copy change) | operator rule; R-560 | | 8.2 | 2026-10-01 | `app_info`: tagline, use_cases, first_steps, default_creds (if any), add_people — and each one TRUE on 9202 | read on 9202's app page and follow the first steps | paperless's page named a login that did not exist (R-515); first steps named a literal `wiki.DOMAIN` (R-498) | | 8.3 | 2026-10-01 | Logo + screenshots on felhom.eu by `slug` | `https://felhom.eu/assets/-logo.svg` (or `.png`, the controller's fallback) and `-screenshot-.webp` answer 200 — NOT `-logo.webp`, which the canonical template's comment still names (R-761) | the card is blank otherwise | | 8.4 | 2026-10-01 | README tables, FIRST-ADMIN row, `category`, `catalog_since` | review | REUSE.md §5 | | 8.5 | 2026-10-01 | The website's app count still holds | `ls templates | wc -l` against `felhom.eu/website` claims; record a drift | felhom-app-catalog skill §6 | ## 9. Sign-off | id | since | Check | How | Why | |---|---|---|---|---| | 9.1 | 2026-10-01 | `python3 scripts/catalog_gates.py ` — all green (exit 0) | bench / scratch host (it runs the runtime gate) | the one entry point (R-161) | | 9.2 | 2026-10-01 | A fresh install on 9202 from the drill catalog, as a household and as a stranger, start to finish | 9202 | the walk finds what single checks miss (R-482..R-488 in one evening) | | 9.3 | 2026-10-01 | Every item above done or n/a-with-reason; every finding not fixed is a register row | the record | prose is not a record | | 9.4 | 2026-10-01 | Published to the live catalog in ONE commit with its record | **gate `onboarding`** | a template and its proof travel together | --- ## Row count | group | rows | |---|---| | 0 Fit | 9 | | 1 Images, start command, database | 9 | | 2 Storage and backup | 8 | | 3 Accounts and strangers | 10 | | 4 Health | 5 | | 5 Resources | 5 | | 6 Updates | 5 | | 7 Mail | 1 | | 8 Text and listing | 5 | | 9 Sign-off | 4 | | **total** | **61** |