PWA & offline shell
IAMScouting Network ships as an installable PWA with read-only offline support
for the surfaces a scout most often returns to mid-trip (poor cell coverage in
stadiums, basements, foreign SIMs). Source files:
public/sw.js— service worker, versioniams-v2public/manifest.json— install metadata (start_url: /network, dark theme)public/offline.html— last-resort fallback page (rendered when SW + cached
copies are all unavailable)
components/PWAInstall.tsx— bottom banner (iOS + Android install hints)components/InstallPwaPrompt.tsx— inline "Install app" link in the
/network top nav, surfaces after the 2nd visit when
beforeinstallprompt has fired
Cache strategies (sw.js)
| Pattern | Strategy | TTL | Cache |
|---|---|---|---|
| /_next/static/* | cache-first | 30d | iams-v2-static |
| /icons/, /leaflet/ | cache-first | 7d | iams-v2-static |
| /api/map-tile/* | cache-first | 24h | iams-v2-tiles |
| /api/geocode | cache-first | 24h | iams-v2-api |
| /api/stadiums | cache-first | 1h | iams-v2-api |
| /api/network/{leads,jobs,wanted,trip-match} GET | stale-while-revalidate | 1h | iams-v2-network-api |
| /network, /network/leads, /network/jobs (navigation) | network-first | 5min fallback | iams-v2-pages |
| Other HTML navigations | network-first → /network → /offline.html | — | iams-v2-pages |
| Everything else (RSC payloads, JS, CSS, JSON, images) | passthrough | — | browser HTTP cache |
Why stale-while-revalidate for network APIs
/api/network/leads, /jobs, /wanted, /trip-match change throughout the
day but a scout reopening the app at half-time during away travel mainly
wants to see what they last loaded — not wait on a slow GPRS round-trip. SWR
returns the cached payload instantly, then refreshes the cache in the
background so the next view is fresh.
Routes that never get cached
The NETWORK_ONLY_PATTERNS list bypasses the worker entirely for anything
auth-sensitive or write-y:
/api/auth/*,/login,/logout,/signup/api/network/dm/,/api/network/pin//api/network/subscribe,/api/network/billing-portal/api/me/,/api/feedback//api/stripe/*/account/,/admin/
Any request with an Authorization header is also passed straight to the
network, and non-GET methods are never intercepted.
Offline-supported routes (after first visit)
These pages load read-only when the device is offline, showing the last cached
content. Identity, DM, and Stripe flows are intentionally NOT in this list —
they go straight to the network and surface /offline.html if it fails:
| Route | Behaviour offline |
|---|---|
| /network | last cached HTML; pins + map + DM list shown read-only |
| /network/leads | last cached HTML + last SWR'd /api/network/leads payload |
| /network/jobs | last cached HTML + last SWR'd /api/network/jobs payload |
| /network/wanted | last cached HTML + SWR'd /api/network/wanted payload |
| /network/trip-match (when reachable) | SWR'd payload |
| /offline.html | always available — branded fallback w/ Try again |
Posting a pin, sending a DM, applying to a job, etc. is not supported
offline — those routes are bypassed and will fail at submit time. The UI
already shows a generic error toast in that case; future work could queue
mutations via Background Sync (low-priority).
Install affordances
- Bottom banner (
PWAInstall.tsx): fixed-position dialog. iOS Safari
gets an "Add to Home Screen" tip; Android/Chromium gets a one-tap install
triggered by the stashed beforeinstallprompt event. Dismiss state
persisted in localStorage (iams-pwa-ios-dismissed,
iams-pwa-android-dismissed).
- Inline link (
InstallPwaPrompt.tsx): subtle "Install app" button in
the /network top nav. Visible only when ALL of:
- beforeinstallprompt has fired (so iOS — which never fires it — does
not see this link)
- localStorage['iams-install-seen'] ≥ 2 (visit count)
- display-mode: standalone is FALSE (not already installed)
- the bottom banner has not been dismissed
- Manifest (
manifest.json):start_url: /network,display: standalone,
icons 192/512/1024 maskable, dark theme #0a0a0a / accent #c79015,
shortcuts to World map / My pins / DMs.
Bumping the SW version
SW_VERSION is the cache namespace. Old caches are purged on activate. Bump
the version any time the cache strategy or the offline page shape changes
(otherwise users keep serving stale content from the previous matrix).
Testing offline
- First visit: load
/network,/network/leads,/network/jobswhile
online. This populates the page + SWR caches.
- DevTools → Network → Offline → reload
/network→ expect the cached
page; /api/network/leads should also serve from cache.
- Navigate to a URL never visited (e.g.
/network/leads/999999) while
offline → expect /offline.html.
- Toggle back online →
/offline.htmltriggerswindow.location.reload()
on the online event so the user lands back on the real page.