DFIELDSOLUTIONS

From the archive

LabReact Native offline-first: SQLite + sync that works

An offline-first React Native app isn't a simple cache. The 4-layer sync architecture we ship · SQLite, op-log, conflict resolution, background sync. With code.

From the archive6 sections · 3 min readBy Dezső Mező · Published .mdMobileReact NativeOffline-firstSQLiteSyncmobile

An offline-first app survives subway tunnels, rural 2G, and airplane mode without the user noticing. That is not a React Query staleTime feature. It's a 4-layer architecture: local persistence, operation log, sync protocol, conflict resolver. Skip any one and the app feels broken.

01Layer 1 · Local persistence (SQLite)

Expo SQLite or react-native-quick-sqlite (faster, unmounts cleanly). Persist the full domain model, not just the UI cache. The SQLite schema mirrors the server schema with two extra columns: _local_updated_at, _sync_state (synced / dirty / conflicted).

CREATE TABLE tasks (
  id TEXT PRIMARY KEY,
  title TEXT NOT NULL,
  completed INTEGER NOT NULL DEFAULT 0,
  updated_at INTEGER NOT NULL,
  _local_updated_at INTEGER NOT NULL,
  _sync_state TEXT NOT NULL DEFAULT 'synced'
);

02Layer 2 · Operation log

Every user action writes an operation to a local queue: { op: 'update_task', payload: { id, fields }, client_id, ts }. This log is what syncs to the server when online. Storing ops (not rows) preserves intent, which matters for conflict resolution.

03Layer 3 · Sync protocol

Server accepts a batch of ops, returns (a) ops that succeeded with their server-assigned timestamps, (b) conflicts with the current server state, (c) any ops from other clients the caller missed. We implement this as a POST /sync endpoint that's idempotent by client_id + ts.

04Layer 4 · Conflict resolver

Last-write-wins (LWW) is the default · server timestamp breaks ties. For fields that must never be overwritten silently (amounts, approvals), we implement CRDT-style operation merging. Mobile-side, a conflict surfaces as a banner: 'Server updated this task. Keep yours? [Keep] [Replace]'.

05Background sync on iOS + Android

Expo BackgroundTask (iOS) and BackgroundFetch (Android) give us ~15-minute minimum periodicity. Trigger a sync on app foreground + on connectivity restore + on a periodic timer · three code paths, all hitting the same sync function.

import NetInfo from "@react-native-community/netinfo";
import { syncNow } from "./sync";

NetInfo.addEventListener((state) => {
  if (state.isConnected && state.isInternetReachable) {
    syncNow().catch((e) => console.warn("sync failed", e));
  }
});

06Edge cases worth handling

  • User switches accounts · nuke the SQLite DB, not just the auth token.
  • Device clock is wrong · sync with a server-timestamped response, never trust client clocks for tie-breaking.
  • Long offline period · an op-log with 500+ queued ops sends fine but takes a noticeable moment; show a progress indicator.
  • Corrupt SQLite file · handle PRAGMA integrity_check on startup and fall back to re-download.
  • Large binary content (images, PDFs) · never in the op-log; upload to object storage, reference by URL.

Measure offline-mode success rate in production: how many actions succeeded without an immediate network round-trip? If it's below 95%, your app isn't offline-first · it's offline-tolerant.

What to take away

  • Offline-first architecture in React Native is not a single library · it's four coordinated layers (local persistence, operation log, sync protocol, conflict resolver). The architecture, trade-offs, and real code samples.
We build this for clientsFull-stack

More from the lab

Browse all entries

Want this looked at on your own system?Start a conversation

5.0
“New shop, more orders in the first week.”Müller Márk · GeekCornerManaging Director · Hungary