Table of Contents
33 — Offline, cache and app updates
Technical reference. For task-based help, see the corresponding user guide.
Documentation status: reviewed against implementation · Reviewed: 2026-07-21 · Primary users: family and coach · Surfaces: mobile web, native wrapper and platform API
Purpose
The product has three complementary resilience layers: a read cache for recently viewed family/activity data, a family-only outbox for idempotent activity creates, and a verified native content cache for the mobile-web application itself. Update policy then guides users to either restart downloaded web content or install a newer native wrapper.
Capability boundaries
| Layer | What it does | What it does not do |
|---|---|---|
| Activity response cache | Shows recent summary/activity data when refresh fails offline or with a server error. | It is not an offline database and does not authorize stale access. |
| Activity-create outbox | Saves eligible self-family activity creates for later replay. | It does not queue edits, deletes, account changes or arbitrary file uploads. |
| Native mobile-web cache | Runs a downloaded, checksum-verified web release from local storage. | It does not cache the platform API or bypass API compatibility. |
| Native update policy | Classifies wrapper version as current/recommended/required and opens the correct store. | It does not install an App Store/Play update itself. |
Cached reads
- Family summaries, activity pages and complete analysis loads are persisted in versioned local storage.
- Scope includes API environment, actor user and authentication session, preventing one account/environment from reading another scope's cache.
- Fresh summary lifetime is 5 minutes; fresh activity/page lifetime is 6 hours; all stale fallback data is discarded after 24 hours.
- Network fetch rejection/offline or API 5xx may fall back to still-readable stale data. Authentication, permission, validation and other client errors do not use stale fallback.
- Successful writes update/invalidate affected cached data. Sign-out/session change clears or moves to a different scoped cache.
- A failed background refresh displays Showing cached data with Retry; corrupt/unavailable/quota-limited storage is ignored because cache is an optimization.
Offline activity-create outbox
1. Save locally
- Only a genuine family actor writing their own family context is queue-eligible.
- The serializable activity-create request receives a UUID
clientMutationIdbefore the first direct attempt, so an uncertain response and later replay cannot create a duplicate server record. - When a retryable connectivity/timeout failure occurs, the request is stored as
pendingwith its family/baby/activity ordering scope and any local timer IDs that must be suppressed. - UI copy distinguishes Saved on this phone. Waiting to sync from Waiting to retry.
2. Replay safely
- Replay runs on online, focus/visibility and authenticated API-success signals plus a 15-second sweep.
- A cross-tab lease prevents two browser contexts from replaying the same scope simultaneously.
- Entries are ordered per family + baby + activity type; a failing older entry blocks later entries in that ordering scope so history is not reordered silently.
- Network/server failures back off and remain pending. An authentication failure pauses replay until a later authenticated API success.
- The server's
clientMutationIdcontract makes create replay idempotent, including “already saved then later deleted” terminal handling.
3. Ask for attention when unsafe
- Permission/access change, missing family/baby, validation rejection, idempotency conflict/result-gone or an unclassified permanent failure moves an entry to
needsAttention. - Any entry still automatic after seven days is also paused for explicit review.
- Tracking and shell surfaces provide Retry, Review and Discard. Discarding a queued timer-backed activity announces suppressed timer IDs so local/native timer restoration cannot resurrect it.
- Signing out with saved entries warns that they will be discarded; if safe local deletion fails, sign-out is stopped for retry.
Timer reconciliation
Active timers have local/native continuity and server state. A stopped timer that becomes an outbox activity keeps its mutation identity and suppresses the corresponding local timer while pending. Fresh server summaries win when newer; discard/replay signals prevent duplicate closure or reappearance. This protects continuity but does not make timers a general offline multi-device consensus system.
Mobile-web release startup
Release selection and verification
- Production wrapper requests
GET /mobile-web/releases/latest?channel=<configured>&wrapperVersion=<native version>. - Only
publishedrecords are candidates. The platform sorts newest first and selects a release whose wrapper SemVer range and the serving API's compiled contract version are compatible. - Manifest supplies web release version, zip URL, SHA-256, entry file, required wrapper/ranges and release date.
- Wrapper downloads the zip, computes SHA-256, rejects a mismatch, extracts into
DocumentDirectory/mobile-web/releases/<version>, and writes the current release pointer only after successful installation.
Startup states
| State | Result |
|---|---|
| Debug with dev URL | Uses the mobile-web development server and reports web version dev. |
| Online, no cache, compatible release | Downloads/verifies/installs before opening the WebView. |
| Offline, valid cache | Starts the cached local release. |
| Offline, no cache | Shows first-launch offline state; there is no unverified remote fallback. |
| Manifest/install failure with valid cache | Rolls back/continues with cache and retains a warning. |
| Cached release + newer compatible release | Serves current cache now, downloads/verifies new content in background, then advertises it as installed. |
| Incompatible wrapper | Surfaces update-required/incompatible state; another channel is not substituted. |
The local static server opens the entry file with ?webReleaseVersion=<version> for cache busting. The bridge exposes only platform, wrapper version, web release version and initial path; the wrapper does not persist Supabase tokens.
Web and native update UX
Downloaded web update
- Wrappers at least
4.5.0supportrelease.checkForUpdateandrelease.restartWithInstalledUpdate. - On family/coach home, mobile checks in the background. Once the wrapper has installed a newer compatible zip, the home Update control restarts the local server/WebView into that release.
- Restart failure attempts to restore the previous release and leaves a retryable warning/update state.
Native wrapper update
- Mobile calls
GET /native-app/updates/check?channel=production&platform=ios|android&wrapperVersion=…on home. - The newest active matching policy yields
currentwhen at/above latest,updateAvailable/recommendedwhen below latest, orupdateRequired/requiredwhen below minimum. - Active policies require valid SemVer, minimum ≤ latest and the platform's App Store ID/Android package name. Only super-admin manages policies.
- Native update takes display priority over a pending web update. If the user declines/returns from the store prompt, the already-downloaded web update can be offered next; a later policy recheck that becomes required restores native priority.
- Current UI uses a confirm/store-opening flow for both recommended and required responses. “Required” is policy severity and prioritization, not yet a hard in-app lock.
Validation and user-visible states
| State | Behaviour |
|---|---|
| Stale cached activity | Warning identifies cached content and offers retry. |
| Outbox pending | Global/activity indicator reports saved count and sync/retry state. |
| Outbox needs attention | Reason-specific access/missing/conflict/validation/7-day copy plus retry/discard. |
| Storage quota/corruption | Cache/outbox persistence failure is contained and surfaced where durability cannot be guaranteed. |
| Bad zip/hash/entry | Release is not promoted; valid prior cache remains. |
| Web update installed | Home Update restarts locally; no store visit. |
| Native update available/required | Confirmation opens App Store/Google Play; failure tells the user to update directly. |
Platform API contract
| Method and route | Purpose |
|---|---|
| Activity GET/create routes | Cache source of truth and idempotent clientMutationId activity creation. |
GET /mobile-web/releases/latest | Public manifest selected for channel, wrapper and serving API compatibility. |
GET/POST/PATCH /admin/mobile-web-releases… | Super-admin release registry operations. |
GET /native-app/updates/check | Public platform/channel/wrapper policy classification. |
GET/POST/PATCH /admin/native-app-update-policies… | Super-admin native update policy management. |
Security, integrity and operational rules
- Cached business data is local convenience scoped to actor/auth/API and expires; current API authorization must win whenever reachable.
- Offline replay never broadens actor scope and every replay is authorized/validated by the platform.
- Release SHA-256 and wrapper/API ranges are mandatory integrity/compatibility gates, not advisory metadata.
- A bad production web release should be paused in the registry so selection rolls back; do not mutate an old artifact under the same version.
- Native wrapper and web release are separate versions. A web build needing a new native bridge must exclude old wrappers through its compatibility range.
Offline support is intentionally narrow: cached reads and self-family activity creates. Never promise that edits, deletes, attachments or all product screens work offline, and never turn a stale-cache response into permission to show data after an explicit 401/403.
Acceptance checklist
- [ ] Cache is actor/session/environment scoped with 5-minute summary, 6-hour activity and 24-hour maximum stale limits.
- [ ] Only network/5xx failures use stale fallback; auth/permission failures remain authoritative.
- [ ] Queued creates retain one
clientMutationId, cross-tab lease and ordering guarantees. - [ ] Permanent/7-day entries require attention and retry/discard handles timer suppression.
- [ ] Sign-out cannot silently leave another user's queued activity visible.
- [ ] First online install and every update verify SHA-256 before promotion.
- [ ] Offline/failed update uses a valid cache; no-cache first launch fails safely.
- [ ] Release selection enforces exact channel plus wrapper and serving-API compatibility.
- [ ] Web restart requires wrapper ≥4.5.0 and can restore the previous release on failure.
- [ ] Native policy distinguishes recommended/required accurately without documenting a nonexistent hard lock.
Source evidence
- Mobile web:
mobile-web/src/api/activity-cache.ts,mobile-web/src/offline/activity-create-outbox.ts,ActivityCreateOutboxContext.tsx,activity-create-outbox-runner.ts,mobile-web/src/routes/ActivityTrackingScreen.tsx, update handling inmobile-web/src/App.tsx - Native wrapper:
mobile-wrapper/src/release/releaseResolver.ts,nativeReleaseDeps.ts,manifest.ts,mobile-wrapper/src/hooks/useMobileWebRelease.ts - Platform:
platform/packages/shared/src/mobile-web-releases.ts,native-app-updates.ts,platform/apps/api/src/routes/mobile-web-release-routes.ts,native-app-update-routes.ts, release/update services
