cinny/nginx.conf means to never cache HTML (the "Never cache HTML or JSON (index.html, config.json, manifest.json)" block), and the service worker relies on it (src/sw.ts, "DEPLOY-SAFETY INVARIANTS: index.html / navigations are NEVER precached … a new deploy is picked up immediately"). But SPA URLs reach index.html through location / { … rewrite ^(.+)$ /index.html break; }. With break, nginx serves the file inside location /, which sets no cache headers. The \.(json|html)$ location never sees the request.
Live headers (2026-10-05):
GET / 200 last-modified: Sat, 03 Oct 2026 04:53:00 GMT (no Cache-Control, no Expires)
GET /home/ 200 last-modified: … (no Cache-Control, no Expires)
GET /login/matrix.lotusguild.org/ 200 last-modified: … (no Cache-Control, no Expires)
GET /index.html 200 cache-control: no-cache / no-cache, no-store, must-revalidate
GET /assets/index-DL_PmJL1.js 200 cache-control: max-age=31536000 / public, immutable
With only Last-Modified, browsers apply heuristic freshness, typically 10 % of the document's age. A shell last modified two days ago can be reused without asking for about 5 hours.
lotus_deploy.sh:163 deploys with rsync -a --delete … dist/ "$WEBROOT/", which removes the previous build's hashed assets. A browser that reuses a heuristically fresh shell after a deploy therefore requests assets/index-<old>.js, which 404s, and the user gets a blank or broken app until the cached shell expires. Users whose service worker still has the old precache are spared; first-time or SW-less visitors (private windows, Firefox with storage blocked, cleared SW) are not. Opening the PWA or a bookmarked /home/ link is the typical trigger, while a manual reload revalidates.
Fix (any one):
Add the no-cache headers in location / too (add_header Cache-Control "no-cache" always;, plus the security headers include, since add_header doesn't inherit). Or route the rewrite to a named location that has them.
Keep the previous build's assets/ for a day after a deploy (drop --delete for assets/, prune old files on the next run).
Also noticed:
Responses carry two Strict-Transport-Security headers (max-age=63072000; includeSubDomains; preload and max-age=63072000; preload). Browsers honour only the first; keep one, probably at a single layer (snippet or NPM, not both).
Two Cache-Control headers (expires -1 already emits Cache-Control: no-cache, then add_header adds no-cache, no-store, must-revalidate). Harmless, but dropping expires -1 keeps it to one.
sw.js, config.json, manifest.json, the call page (call.chat.lotusguild.org/public/element-call/index.html, frame-ancestors https://chat.lotusguild.org) and hashed assets (1 y, immutable) are all set correctly.
Found in the October 2026 client audit (Playwright against a local Synapse + LiveKit stack, plus code reading). Code refs are at cinny 30e2dbcd, cinny-desktop 8f67ec7, element-call 3b0050e8.
`cinny/nginx.conf` means to never cache HTML (the "Never cache HTML or JSON (index.html, config.json, manifest.json)" block), and the service worker relies on it (`src/sw.ts`, "DEPLOY-SAFETY INVARIANTS: index.html / navigations are NEVER precached … a new deploy is picked up immediately"). But SPA URLs reach `index.html` through `location / { … rewrite ^(.+)$ /index.html break; }`. With `break`, nginx serves the file **inside `location /`**, which sets no cache headers. The `\.(json|html)$` location never sees the request.
**Live headers (2026-10-05):**
```
GET / 200 last-modified: Sat, 03 Oct 2026 04:53:00 GMT (no Cache-Control, no Expires)
GET /home/ 200 last-modified: … (no Cache-Control, no Expires)
GET /login/matrix.lotusguild.org/ 200 last-modified: … (no Cache-Control, no Expires)
GET /index.html 200 cache-control: no-cache / no-cache, no-store, must-revalidate
GET /assets/index-DL_PmJL1.js 200 cache-control: max-age=31536000 / public, immutable
```
With only `Last-Modified`, browsers apply **heuristic freshness**, typically 10 % of the document's age. A shell last modified two days ago can be reused without asking for about 5 hours.
`lotus_deploy.sh:163` deploys with `rsync -a --delete … dist/ "$WEBROOT/"`, which **removes the previous build's hashed assets**. A browser that reuses a heuristically fresh shell after a deploy therefore requests `assets/index-<old>.js`, which 404s, and the user gets a blank or broken app until the cached shell expires. Users whose service worker still has the old precache are spared; first-time or SW-less visitors (private windows, Firefox with storage blocked, cleared SW) are not. Opening the PWA or a bookmarked `/home/` link is the typical trigger, while a manual reload revalidates.
**Fix (any one):**
- Add the no-cache headers in `location /` too (`add_header Cache-Control "no-cache" always;`, plus the security headers include, since `add_header` doesn't inherit). Or route the rewrite to a named location that has them.
- Keep the previous build's `assets/` for a day after a deploy (drop `--delete` for `assets/`, prune old files on the next run).
**Also noticed:**
- Responses carry **two `Strict-Transport-Security` headers** (`max-age=63072000; includeSubDomains; preload` and `max-age=63072000; preload`). Browsers honour only the first; keep one, probably at a single layer (snippet or NPM, not both).
- **Two `Cache-Control` headers** (`expires -1` already emits `Cache-Control: no-cache`, then `add_header` adds `no-cache, no-store, must-revalidate`). Harmless, but dropping `expires -1` keeps it to one.
- `sw.js`, `config.json`, `manifest.json`, the call page (`call.chat.lotusguild.org/public/element-call/index.html`, `frame-ancestors https://chat.lotusguild.org`) and hashed assets (1 y, immutable) are all set correctly.
---
_Found in the October 2026 client audit (Playwright against a local Synapse + LiveKit stack, plus code reading). Code refs are at cinny `30e2dbcd`, cinny-desktop `8f67ec7`, element-call `3b0050e8`._
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
cinny/nginx.confmeans to never cache HTML (the "Never cache HTML or JSON (index.html, config.json, manifest.json)" block), and the service worker relies on it (src/sw.ts, "DEPLOY-SAFETY INVARIANTS: index.html / navigations are NEVER precached … a new deploy is picked up immediately"). But SPA URLs reachindex.htmlthroughlocation / { … rewrite ^(.+)$ /index.html break; }. Withbreak, nginx serves the file insidelocation /, which sets no cache headers. The\.(json|html)$location never sees the request.Live headers (2026-10-05):
With only
Last-Modified, browsers apply heuristic freshness, typically 10 % of the document's age. A shell last modified two days ago can be reused without asking for about 5 hours.lotus_deploy.sh:163deploys withrsync -a --delete … dist/ "$WEBROOT/", which removes the previous build's hashed assets. A browser that reuses a heuristically fresh shell after a deploy therefore requestsassets/index-<old>.js, which 404s, and the user gets a blank or broken app until the cached shell expires. Users whose service worker still has the old precache are spared; first-time or SW-less visitors (private windows, Firefox with storage blocked, cleared SW) are not. Opening the PWA or a bookmarked/home/link is the typical trigger, while a manual reload revalidates.Fix (any one):
location /too (add_header Cache-Control "no-cache" always;, plus the security headers include, sinceadd_headerdoesn't inherit). Or route the rewrite to a named location that has them.assets/for a day after a deploy (drop--deleteforassets/, prune old files on the next run).Also noticed:
Strict-Transport-Securityheaders (max-age=63072000; includeSubDomains; preloadandmax-age=63072000; preload). Browsers honour only the first; keep one, probably at a single layer (snippet or NPM, not both).Cache-Controlheaders (expires -1already emitsCache-Control: no-cache, thenadd_headeraddsno-cache, no-store, must-revalidate). Harmless, but droppingexpires -1keeps it to one.sw.js,config.json,manifest.json, the call page (call.chat.lotusguild.org/public/element-call/index.html,frame-ancestors https://chat.lotusguild.org) and hashed assets (1 y, immutable) are all set correctly.Found in the October 2026 client audit (Playwright against a local Synapse + LiveKit stack, plus code reading). Code refs are at cinny
30e2dbcd, cinny-desktop8f67ec7, element-call3b0050e8.