Designing an Offline-First Architecture
The client’s wellness app is used throughout the day—gyms with weak signal, commutes through tunnels, clinics with spotty Wi‑Fi. Users still need to log water, sleep, exercise, and meals without losing progress or feeling punished for being offline. The product goal was not a full local database replica, but reliable daily quest logging with honest UX about what syncs now versus later.
We built a pragmatic offline-capable layer on React Native: TanStack Query with offlineFirst reads, MMKV-backed cache persistence, and paused mutations for quest submissions. Food logging received extra treatment—local camera URIs are stored in the quest payload and reconciled on reconnect—because photos and AI analysis cannot run without network.
The Problem
Connectivity is intermittent, habits are not
Users open the app to complete short daily quests. If submission fails silently or blocks the UI, they stop logging. If the app pretends everything synced when it did not, support and data integrity suffer.
Not every feature can work offline
AI food analysis, coin claims, and server-side validation require network. The architecture had to separate “record locally” from “claim rewards” and from “analyze with AI.”
Food is the hardest offline case
A meal log is more than a JSON field: it includes a camera capture, optional gallery metadata, and a server upload step. Skipping AI offline is acceptable; losing the photo or duplicating entries is not.
Requirements we had to meet
- Let users complete daily quest logs while offline when possible.
- Persist read caches across app restarts so yesterday’s quest list is still visible.
- Resume writes automatically when connectivity returns.
- Show clear status (offline, connecting, reconciling)—not silent failure.
- Encode product rules in UI: no coin claims offline, food limited to today when offline, daily tab hidden if there is no cached data.
The Solution
Three layers: detect, cache, pause-and-resume
1. Network detection — NetInfo drives a global banner and treats the app as offline unless the device has both a link and reachable internet. TanStack’s onlineManager is updated on every net change so queries and mutations share one source of truth.
2. Cache-first reads — React Query defaults to networkMode: 'offlineFirst' with a 24-hour gcTime. The entire query cache is persisted to MMKV via the React Query persist client, so dashboards and quest lists can render from disk after a cold start.
3. Paused mutations for quest writes — Daily quest submissions use one shared write mutation to the quest API. When offline, mutateAsync pauses instead of throwing; screens detect a paused mutation and show completion messaging that tells users to claim rewards once back online.
On cold start, the persisted query client runs resumePausedMutations() after cache restore, then invalidates queries—so the same resume path works for session reconnect and app relaunch (for mutations still in memory).
Quest submission pattern
Every major daily quest screen (water, sleep, exercise, mood, period, bowel, weight, food) follows the same contract:
- Submit the quest payload through the shared write mutation.
- If the mutation is paused, show an offline completion toast: “You recorded a {type} log — claim the rewards once you're back online.”
- Loading spinners only run while the mutation is pending and not paused, so the UI does not spin indefinitely while waiting for connectivity.
Coin claims are blocked separately: the quest card shows an explicit offline message when users try to claim points without connectivity.
Food logging: deferred photo reconcile
Offline food flow is intentionally different:
- No AI while offline — image and text analysis mutations are skipped; the user still saves a minimal meal entry with the photo kept on the device until sync.
- Submit with the quest write — the log is stored locally with a pending-photo flag so the app knows the image still needs to reach the server.
- On reconnect — after the connectivity banner finishes, a dedicated reconcile step runs: reload the food quest, upload any pending photos, clear the pending flag on each record, then submit the updated quest data in one pass.
Gallery picks while offline are restricted to today’s meals (photo date metadata); past-day food logs hide the log-meals action when offline.
Global offline UX
A connectivity provider surfaces a sliding banner for offline, connecting, connected, and reconciling states. After reconnect, the banner cycle finishes and the food photo reconcile step runs automatically.
The daily quest tab stays hidden when there is no cached quest list and the device is offline—so users see an explicit “not available offline” message instead of empty cards.
Key Outcomes
- Unified quest write path — one quest API with paused mutations across all daily quest types.
- Surviving reads offline — MMKV-persisted React Query cache with
offlineFirstnetwork mode. - Transparent reward timing — users complete logs offline but know coins wait for connectivity.
- Food photos don’t block logging — local photo path on the record plus dedicated reconcile on reconnect.
- Product rules in UI — coin claims, food date limits, and daily tab gating encoded where users act.
What Changed for Users
| Without offline design | With offline-capable layer |
|---|---|
| Submit fails or spins forever | Quest logs pause and resume; clear “claim when online” copy |
| Blank screens offline | Cached quest data when available; explicit message when not |
| Food camera unusable offline | Stub log + photo retained; AI and upload catch up on reconnect |
| Ambiguous sync state | Banner for offline / connecting / reconciling |
Trade-offs & Lessons
What worked: Reusing TanStack Query’s pause/resume avoided a custom outbox table. MMKV persistence was fast enough for mobile. A single dedicated food-photo reconcile step kept the special case localized.
What we’d harden next:
- Align TanStack
onlineManagerwith the UI offline definition: the banner requires reachable internet, while the query client may still consider the device online on captive portals. - Persist paused quest mutations with explicit
mutationKeys if writes must survive process kill, not just reconnect in-session. - User-visible errors when food photo reconcile fails.
- Broader offline coverage for bookings, wallet, and scale sync—currently network-dependent.
Stack: React Native · Expo · TanStack Query · MMKV · NetInfo · Expo Camera