# caddie — v8 substrate feedback

> Running log of friction hit while building **inside** the v8 conventions.
> **This is feedback about the substrate, not about this project** — it gets
> compiled into `/srv/.logs/planning/server-evolution-2026.md` or the ops queue
> (`/srv/.logs/ideas.md`) once the build settles.
>
> Scope discipline: only entries where the *server framework, standards library,
> or gateway* caused the friction. Bugs in this project's own code belong in
> `handoff.md`, not here.
>
> Each entry: what happened, what it cost, and a candidate fix.
> (Pattern proven 2026-07-30; it is what turns one project's
> pain into a standards fix for every later project. Delete this file if the
> project never hits substrate friction.)

---

## 2026-09-02 — the template serves the SPA shell with no Cache-Control

**What happened.** After deploying caddie 0.4.0, a plain visit to `/` kept
rendering the PREVIOUS bundle; a cache-busted URL showed the new one. The
scaffolder's `app-template/main/app/main.py` `spa()` returns
`FileResponse(INDEX_HTML)` with no cache header, so browsers (and any CDN in
front) may reuse a stale shell — and the shell names the content-hashed
bundles, so once an image stops carrying old bundles (`.dockerignore`,
caddie 0.3.2) a stale shell doesn't just look old, it 404s its own JS.

**What it cost.** A confusing visual pass (the "old UI" after a green deploy)
and, for a client, a broken page until a hard reload. Every scaffolded app
has it.

**Candidate fix.** `FileResponse(INDEX_HTML, headers={"Cache-Control":
"no-cache"})` in the template's `spa()` — revalidate the shell on every load;
the hashed `/assets/*` stay cacheable. Caddie 0.4.1 carries the fix
(`tests/test_shell.py` pins it).

## 2026-09-02 — two gaps every scaffolded app inherits from the kit

**1. `/api/bw/me` carries no display name.** Every app that wants to speak to
the person ("Hi Dana") re-derives a name from the username, or — as caddie
now does — calls the kit's own `bw_auth.userinfo()` per user with a cache.
The kit already has `first`/`last` from central at invite time and on every
userinfo call; `me_payload` could carry `first_name` and `display_name` for
free. *Cost so far:* one workaround per app (easel derived from the username;
caddie built `app/services/profile.py`). *Candidate fix:* add the two fields
to `bw_admin_api.me_payload` and to the react pack's `Me` type.

**2. The react-admin pack's `bwApi.ts` reads only `{detail: {...}}`.** The
kit's own middleware answers a TOP-LEVEL `{error_code, summary, details}`
(401 signed out; 403 read-only View As), so a person in a View As who tries a
write sees "Request failed (403)" instead of the sentence the middleware
wrote for exactly that moment. Every scaffolded app has the gap unless its
client was written by hand (caddie's `apiError()` reads both). *Candidate
fix:* `bwApi.ts` parses both shapes and exposes `details`; UI-STANDARD.md
states the rule.

## 2026-08-24 — the scaffolder's Dockerfile runs every BW app as root

**What happened.** Adding an upload surface (comment attachments) prompted a
look at what uid receives those bytes: `main/Dockerfile` has no `USER`
directive, so the container runs as uid 0. This is not a caddie choice —
`/srv/system/id-auth/app-auth/app-template/main/Dockerfile` ships no `USER`
either, so **every app the scaffolder stamps runs as root**. Two older
hand-built apps on this box do drop privileges (`USER <app>` after a
`chown -R`), which is how the gap was visible at all.

**What it cost.** Nothing yet — no incident, and it doesn't block the M1 gate.
The cost is latent and grows with each app: a file-upload endpoint, an image
parser, or any dependency CVE lands with uid 0 inside the container. Files
written to the bind-mounted `data/` also land root-owned, so the owner's own
shell can't clean them up without sudo.

**Candidate fix (needs the owner — `/srv/system/` is operating-manual
territory).** Add to the scaffolder's runtime stage:

```dockerfile
ARG APP_GID=1000
RUN groupadd -g "${APP_GID}" appgrp && useradd -u 10001 -g appgrp -M appuser \
 && mkdir -p /data && chown -R appuser:appgrp /app_root /data
USER appuser
```

with `APP_GID` set to the project's `<name>-dev` gid, so the non-root user can
still write the setgid, ACL'd workspace `data/` dir. The two apps that already
do this don't bind-mount a host `data/`, which is why their simpler
`USER <app>` pattern isn't directly copyable — the gid match is the part the
standard needs to state.

## 2026-08-20 — new-bw-app.sh writes `.app.env` unreadable by the gateway

**What happened.** On a fresh scaffold (`punchlist`), the first
`srv-gw deploy` failed with `open /srv/apps/<name>/.app.env: permission
denied`. The scaffolder writes `.app.env` as 0600 owned by the running user
(rian), but compose consumes it via `env_file:` and the gateway runs the
deploy — react.md's own rule ("the gateway must be able to read every file
compose needs": 660 group `<project>-dev`, or 600 srv-gateway).

**What it cost.** One failed deploy + a couple of minutes. Predictable for
every future BW app.

**Candidate fix.** `new-bw-app.sh` should write `.app.env` as 0660 group
`<name>-dev` when the project group exists (it scaffolds inside a gateway
project, so it always does). One-line change at the "wrote .app.env" step.
