MyVitals
Engineering

Two Caches, One WebView, and a Deep Distrust of navigator.onLine

Why MyVitals runs a Workbox service worker AND a hand-rolled IndexedDB cache for the same data, why navigator.onLine lies, and the profile-scoping bug that comes with multi-tenant offline caching. ---

S

Suyash Vashishtha

3 September 2026 · 2 min read

ShareWLF
Two Caches, One WebView, and a Deep Distrust of navigator.onLine

MyVitals' mobile presence is a native WebView pointed at a remote URL — not bundled locally. If the phone loses signal in an elevator, a naive setup just shows nothing, forever, until signal returns. For an app about checking your health data anywhere, that's a bad joke.

Layer one: a service worker

Workbox precaches the app shell and runtime-caches API responses — standard stuff, if the service worker reliably registers and persists inside a native WebView, which varies by platform version and isn't something you fully control. So it's layer one, not the whole plan.

Layer two: the redundant one that actually saves you

A hand-rolled IndexedDB write-through cache at the axios level. Every successful GET gets written to IndexedDB; a shared useCachedQuery hook serves from it the instant a request fails instead of showing a spinner into the void:

try {
  const { data } = await apiClient.get(url);
  await idbCache.set(cacheKey(url), data);
  return data;
} catch (err) {
  if (!err.response) return idbCache.get(cacheKey(url));
  throw err;
}

Same data, two storage layers, on purpose. The service worker exists so the app shell loads at all in a WebView that's never had a chance to hit the network first; IndexedDB exists because the service worker might just not be there. A proactive prefetch sweep runs on login/reconnect so the cache is warm before the user goes offline — the goal is "you can't tell you're offline," not "gracefully handle it."

navigator.onLine will lie to your face

It reports whether the network interface is up, not whether it can reach anything — worse inside a WebView. Connectivity is a real network probe (GET /ping.txt, uncached), not a property read.

Writes are where I drew a hard line

Reads work offline. Writes don't — no offline write queue anywhere. Offline uploads/edits fail loudly with an inline message instead of queuing silently. This was deliberate: an offline write queue for a health app means conflict resolution, partial-sync failures, stale-queue edge cases — weeks of work for a single developer. Silent data loss is worse than an honest error, especially with someone's cholesterol on the line.

The bug waiting to happen: profile scoping

Both cache layers key on (endpoint, activeProfileId), not just the endpoint. MyVitals has family sharing — one login can "act as" a family member. Cache purely by URL and switching profiles means briefly showing your mother's cached dashboard under your session, or leaking yours into hers. If you're building offline support for anything multi-tenant, this is the bug that doesn't show up in your demo.


👉 Try MyVitals now — load your dashboard, then flip on airplane mode.

Track your own lab reports

Upload a report from any lab, in any format. MyVitals reads every value and builds one continuous history you can actually follow.

General health information, not medical advice. Always consult a qualified healthcare professional.

Related reading