# The web app

2026-08-08. Ernest:

> "Also add to the roadmap before cut over is a web app (pwa) like Invision
> has. That is built into the system just like Clubs."

A pre-cutover gate item, alongside the applications, the KB and the assistant.

---

## Why it is part of System and not an application

Everything else on the gate is an application: it owns content, registers
widget kinds and areas, can be switched off, can claim `/`. The web app is
none of those things. It owns no content, has no pages of its own, and there is
nothing coherent to look at with it turned off.

It is a **platform capability** — the site, delivered differently. So it lives
in System, like identity and permissions do, and every site gets it. That is
also the plain reading of "built into the system".

🚨 **"Just like Clubs" is about *first-party and built in*, not about being an
application.** Clubs is on the gate because it ships with the product rather
than as an add-on; the web app is on the gate for the same reason and belongs
in a different layer. Reading it as "make it an application" would produce an
application with no content type, no areas and nothing to disable — a row in
Admin → Modules that does nothing.

## What it has to be

An installable, offline-tolerant version of the site the operator has already
branded. Four parts:

**A manifest**, generated rather than a static file, because everything in it
is the operator's: the name, the icons, the theme colour. A hardcoded
`manifest.json` would put Convoro's name on the home screen of every install —
exactly the defect [the branding note](2026-08-08-logo-and-favicon.md) was
written to fix, repeated one layer out.

**A service worker.** 🚨 The whole risk of this feature lives here. A cached
shell that never updates is the classic PWA failure: the site ships a fix and
some fraction of visitors keep the old one for weeks, with no way to tell them
apart from people on a working version. The cache must be keyed on the same
asset version the stylesheet links already carry, and a new version must take
over rather than wait politely for every tab to close.

**An install prompt that is not a nag.** Shown once, dismissible, and never
again for somebody who said no. A banner that reappears on every visit is the
single most common reason people describe a PWA as spam.

**Honest offline behaviour.** A cached shell saying "you are offline" beats a
browser error page. A cached shell pretending the site is working, and silently
dropping a reply somebody typed, is worse than either.

## The icons are already being generated

The favicon upload built on 2026-08-08 produces a set of square PNGs from one
image. A manifest wants **192** and **512**; the browser tab wants 32 and iOS
wants 180.

🚨 Those two extra sizes were added to `BrandStore::ICON_SIZES` **before this
feature exists**, and that is deliberate. Only the generated sizes are kept —
the original upload is not — so adding a size after the fact would mean every
operator re-uploading their icon to get it. Four small PNGs cost nothing.

The theme accent is already a design token, so `theme_color` is a read rather
than a new setting.

## What this note does not decide

- **Web Push.** It needs VAPID keys, a subscription store, and a sender. The
  sender belongs to the task runner — 🚨 never a visitor's request, which is the
  standing rule with exactly two exceptions and this is not one of them.
  Notifications already exist in-product; push is a delivery channel for them
  and is its own note.
- **How much works offline.** Reading cached pages is clearly worth it. Queuing
  a reply written offline and sending it later is a different feature with real
  failure modes — a post that appears hours later, out of context, under a
  timestamp nobody expects.
- **iOS.** Support is partial and conditional on being added to the home
  screen. Whatever is written about this must say so rather than implying
  parity, because "it does not work on my iPhone" is otherwise the first
  support question.
- **Whether the shell is themed per member.** Theme mode can be `auto`, and a
  cached shell has to pick something before the page loads.
