Accepted

ADR 0061 — Theme fallback is observable

When the public render path falls back to the bundled default theme, it warns and records a KV flag the admin Health and Themes pages surface

The render adapter's editor-first guarantee (ADR 0028, and the themed-404 fallback decision) is that any theme failure falls back to the bundled default templates rather than serving an error page. That is correct runtime behaviour. The gap it leaves: the fallback was silent. An Almanac install signed with a key later removed from the trust registry rendered the whole public site in the bundled fallback while the admin Themes page still showed Almanac as Active with no hint anything was wrong. The trust-failure branch logged nothing at all; the template-load and render-throw branches logged to the console only, which an editor never sees.

Context

Three failure modes drop the render to the bundled default: the active theme's signing key is no longer trusted (install-time verification cannot catch a key rotated out after install, so the render path re-checks per isolate); the theme's compiled template bytes are missing from R2; or the theme throws while rendering. All three are recoverable by re-installing or switching themes, but the editor first has to know it is happening. The public site looking wrong with nothing anywhere explaining why is an editor-first defect.

The decision

  • The trust-failure branch now emits a console warning naming the theme id, version, and reason, matching the console logging the other two branches already had.
  • Every fallback (trust failure, template-not-found with the loader bound, load error, or render-time throw) records a small KV flag at render:theme-fallback holding { themeId, themeVersion, reason, at }. The reason is an editor-facing sentence, not an internal error string.
  • The write is guarded three ways so a broken theme cannot hammer KV: a per-isolate guard means at most one KV touch per isolate per reason; the write only happens when the key is absent; and a one-hour expirationTtl lets the flag self-heal — once the theme renders again nothing re-writes it and it expires. Every KV interaction is wrapped so a KV failure can never affect the render.
  • The local-development case is excluded: under wrangler dev the Dynamic Worker Loader binding is undefined, so every active theme raises template-not-found. That is the expected local condition already surfaced by the Themes page's themedRenderAvailable banner, so the not-found branch records nothing when the loader is unbound.
  • The admin Health page reads the flag and shows an error card headed "Active theme is not rendering" — the public site is on the built-in fallback, the theme name and version, the reason, and what to do (re-install from Themes, or choose another theme). The Themes page cross-checks the flag against the active pointer and flags the offending row with a "Not rendering — the built-in fallback is being served" badge.

Consequences

  • An editor can diagnose a silently-broken theme from the admin instead of guessing why the site looks wrong. The warning clears on its own once the theme renders again, via the KV TTL — no manual dismissal, no stale banner.
  • No per-request overhead on the render path beyond the first fallback in each isolate: the per-isolate guard short-circuits subsequent renders before touching KV. The admin cost is one KV read on the Health page load and one on the Themes list load.
  • The flag is a single most-recent-event record, not a log. If two themes fail in the same window the first-written reason wins until the TTL expires — acceptable because there is one active theme at a time, so the flag names the theme the editor needs to fix.
  • The flag is not a versioned entity; it is transient operational state, so it stays in KV rather than going through the versioned-write helper.
  • Behaviour is exercised end-to-end: render tests assert the flag is written on trust-failure and template-load-failure, and the Health route test asserts the flag is surfaced (and that a malformed value degrades to null).
  • Two follow-through details make the repair loop close for real: the render path's per-isolate trust cache stores only positive verdicts (a pinned negative would keep serving the fallback after a re-install until the isolate recycled), and a successful themed render actively deletes the KV flag, so the Health warning lifts with the repair rather than at TTL expiry.