React NativeOffline-FirstWatermelonDBMobile DevelopmentSupabaseExpo

Building Offline-First Mobile Apps That Never Lose Data (React Native + WatermelonDB)

Qalab Hassnain Agha··11 min read

Apex Rider is a motorcycle tour companion. Its users choose routes precisely because they leave coverage: mountain passes, desert highways, forest tracks. "Works offline" could not be a feature checkbox — turn-by-turn navigation, ride tracking, crash detection, and safety check-ins all had to work with the radio effectively dead, for hours, and lose nothing.

That constraint produces a very different architecture from "add caching later." Here is the shape of it, and the decisions that mattered.

The Inversion: Local Is the Truth

The standard mobile pattern treats the server as the source of truth and the phone as a view over API calls. Offline-first inverts it: WatermelonDB (SQLite) on the device is the truth; the UI reads and writes only local; a sync engine reconciles with the Supabase/Postgres backend whenever a connection happens to exist. The user cannot tell the difference between offline and online, because functionally there is none.

  • Every write commits locally first — recording a ride works identically at zero bars and full bars.
  • Every record carries sync metadata: created/updated locally, synced, or conflicted.
  • The sync engine wakes on connectivity changes, app foreground, and a periodic timer — pushing unsynced batches, pulling remote changes.

What a Ride Actually Looks Like as Data

A three-hour ride is a stream: GPS points every few seconds, sensor windows for crash detection, waypoint events, check-in confirmations. Two properties shaped everything: the data is append-only (points happen; they are never edited), and it is bursty on reconnect (three offline hours can be thousands of rows to push).

  • Client-generated IDs (UUIDs) on every row — the phone cannot ask the server for an ID it may not reach for hours.
  • Batch upserts on sync, idempotent by those IDs — replaying a batch after a dropped connection is harmless; the server deduplicates.
  • Sequence numbers per ride let the server detect gaps and request exactly the missing spans, instead of re-uploading whole rides.

Conflicts: Cheaper Than Advertised, If You Decide Early

Teams fear sync conflicts as if every app were Google Docs. Most are not. In Apex Rider, the overwhelming majority of data is append-only per user — GPS points from your phone conflict with nothing. What remains got explicit, boring rules: last-write-wins for profile and settings; for safety-critical state like an active SOS, the most severe state wins regardless of timestamps. The lesson: enumerate your data types and assign a rule to each before writing the sync engine, not after the first support ticket.

The Details That Bit Us

  • Battery vs fidelity: continuous GPS at full rate kills a phone mid-tour. Adaptive sampling — high rate during turns and speed changes, low on straights — cut battery drain dramatically with negligible track-quality loss.
  • OS background limits: iOS and Android will kill background sync mid-flight. Every stage is resumable; the app assumes it can die at any moment and picks up where it left off.
  • Sync UI honesty: users distrust silence. A small "3 rides waiting to sync" indicator eliminated the "did it save?" anxiety without making anyone think about sync.
  • Offline maps are their own discipline: MapLibre with pre-downloaded route corridors — but that is a full article of its own.

When Offline-First Is Worth It

This architecture costs real engineering: a local database, a sync protocol, conflict rules, and testing regimes most apps never need. It earns that cost when your users genuinely operate without coverage — field work, travel, logistics, healthcare wards, events — or when "it lost my data" is fatal to trust. When a rider finishes a six-hour mountain loop and every kilometre is there, that is the product. If you are building something with the same constraint, this is exactly what my app development practice specialises in — Apex Rider’s full case study is on the services page.

Frequently Asked Questions

What is the difference between offline support and offline-first?

Offline support means the app degrades gracefully without a connection — some cached data remains visible, most actions fail. Offline-first means the app is fully functional offline because all reads and writes go to a local on-device database; network sync is a background process the user never waits on. If airplane mode breaks a core feature, it is not offline-first.

What is the best local database for offline-first React Native apps?

For non-trivial data, WatermelonDB: SQLite under the hood, lazy loading so large datasets stay fast, reactive queries that update the UI automatically, and a sync protocol designed for exactly this architecture. AsyncStorage is fine for preferences; it is not a database. I used WatermelonDB with Expo in a production navigation app streaming GPS points continuously.

How do you handle sync conflicts in offline-first apps?

Per data type, decided at design time. Settings and profile fields: last-write-wins with server timestamps. Append-only streams like GPS tracks: merge unioned batches keyed by client-generated IDs — nothing overwrites anything. Truly concurrent edits to shared records need explicit rules or user resolution, but most mobile apps have far fewer of those than teams fear.

Code, architecture patterns, and recommendations in this article come from real projects but are shared as-is, without warranty — validate them against your own requirements before production use. See the Terms of Use.

Available for Consulting

Let's build something
that matters.

I take on a select number of project-based consulting engagements per quarter — from architecture reviews and LLM pipeline audits to full production builds.

AI SystemsComputer VisionLLM PipelinesMLOpsIoT & BLE

80+ clients · 14+ production systems · Remote / Islamabad