Request lifecycle
Every request to GreyScreen — a catalogue page, a profile, a signed upload — takes the same shape. Once you know these six hops, you can predict where a failure lives before you open a log.
One request, from browser to Firestore and back
Every public call takes the same path. Zoom in to read the labels.
The same system, laid out automatically. Use the controls to zoom, open it full screen, or download it for a deck.
The six hops
| # | Hop | What it does | What can go wrong |
|---|---|---|---|
| 1 | Browser → gateway | TLS terminates; nginx matches the longest prefix | Wrong host, expired certificate, a path that no domain config knows |
| 2 | Gateway → container | Strips the prefix, forwards to the upstream | A route missing from domains.json, or generate.sh not re-run |
| 3 | Container | Static nginx (SPA) or Express (API) handles the now-rooted path | An SPA fallback that swallows /healthz, or a route order bug |
| 4 | API → Firestore | Reads the live snapshot or a document | Wrong Firebase project, a document that is not isReady |
| 5 | Back through the gateway | JSON and cache headers return | compression() buffering a stream, CORS allowlist mismatch |
| 6 | Browser | React renders | A client that assumed a field which is absent for old documents |
Why prefixes matter
The gateway gives each container the path as if it were mounted at its own root. A browser
request to /api/customer/catalog/home arrives at the customer API as /catalog/home. This is
deliberate: the same image can run behind any domain without a path prefix baked into the code.
Consequences worth knowing:
- Debug logs inside the API show the stripped path. Search for it, not for the public URL.
domains.jsonis the list of public routes. A new endpoint is reachable locally and dead publicly until a domain route exists andgenerate.shruns./is not stripped. The website is the only upstream mounted at a bare origin.
Static and API responses
Static frontends answer two things:
/healthz→200with a small body. This is what deploys and monitoring probe.- Everything else →
index.html, so client-side routes work on a hard refresh.
APIs answer GET /health with {status, appEnv, production[, version]}. A deployed build that
returns only {"status":"ok"} predates the current controller. Treat that as version drift,
not a different contract — and re-probe before concluding the environment is broken.
Caching, by surface
| Surface | Policy | Why |
|---|---|---|
| Catalogue and title pages | Short revalidation or no-store | A newly published title must appear promptly |
/catalog/browse | Longer | The ID set is rebuilt server-side, so a stale page cannot hide a deleted title |
| View aggregates (Top 10) | 5 minutes | Expensive to compute, safe to be slightly old |
| Top-10 board membership | Rebuilt from the live snapshot on every request | A deleted or unpublished title must not linger while aggregates are cached |
/health | Not cached | It is a liveness probe |
| Admin SSE | Explicitly unbuffered | Proxies and compression must not sit on the stream |
Identity travels in three different ways
The same request path carries three kinds of identity, and they are not interchangeable:
- Bearer ID token — the website's
/me*routes. Verified withverifyIdToken. userIdin the path or body — the legacy/users/*routes and most native calls. Checks existence only. Do not extend this pattern; see Security notes.- Admin cookie — the back office.
EventSourcecannot send anAuthorizationheader, so the stream route always reads the cookie.
What a healthy request looks like
- One 2xx in the Network tab, with
access-control-allow-originmatching your origin. - The API log shows the stripped path.
/healthnames the environment you expected.
If any of those three is off, fix it before reading the React code.
Next: Upload to publish — where a title comes from.