40% of work happens in basements, substations, and rural sites with no signal for hours.
Losing a completed inspection because the app crashed before sync is a contract failure, not a UX annoyance.
Devices are rugged Android (Zebra TC57, Honeywell CT40) plus a minority of corporate iPhones.
Backend accepts REST batch uploads; conflict policy is server-authoritative on approval timestamps.
Team constraints: Four mobile engineers, one backend engineer, no on-call native specialist. Expo managed workflow with EAS internal distribution to fleet devices.
Pilot fleet: 120 rugged devices on Android 11–14, SDK 57 build 1.6.0.
Metric
Before (online-first prototype)
After (SQLite + outbox)
Lost submissions / week
11 (user-reported)
0 in 8-week pilot
Median save latency offline
N/A (blocked spinner)
<80ms local commit
Sync within 5 min of connectivity
62%
94%
Background task success rate
-
78% (iOS stricter than Android)
Support tickets: "frozen app"
23/month
4/month
Corruption recovery: one device with a failed OTA mid-migration triggered safeParse quarantine - app rendered last-known-good inspections without white-screening per Data Layer Best Practices #13.
SQLite migrations before first paint. A user_version bump shipped in an OTA without a store build bricked cold start once - now native schema changes require binary bump.
Show pending state per row, not global spinner. Inspectors work on job B while job A uploads; per-row synced flag beat a blocking modal.
Rugged devices lie about battery.BatterySaver on Zebra delayed background tasks; document that foreground flush is the real delivery path.
Photo uploads need byte progress.expo-file-system upload with onUploadProgress reduced "did it save?" calls to dispatch.
Logout clears all layers. Stale inspection cache on shared devices was a privacy incident class - queryClient.clear() + SQLite user tables + attachment dirs.
Pin SDK 57 in runbooks. Field IT installs APKs months later; support docs must state which runtimeVersion accepts which OTA.