React Native offline-first · SQLite + sync patterns that actually work
Offline-first is a 4-layer problem, not a feature flag. Here's the architecture we ship across Expo + React Native projects.
Offline-first is a 4-layer problem, not a feature flag. Here's the architecture we ship across Expo + React Native projects.
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.
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'
);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.
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`.
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]'.
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));
}
});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.

By
Founder, DField Solutions
I've shipped production products from fintech to creator-tooling · for startups and enterprises, from Budapest to San Francisco.
Keep reading
RELATED PROJECTS