DField SolutionsMérnöki stúdió · Budapest
Loading · Töltődik
Skip to content
Back to blog
·13 min read
Mobile··13 min read

React Native offline-first: SQLite + sync that works

Offline-first is a 4-layer problem, not a feature flag. Here's the architecture we ship across Expo + React Native projects.

Last verified
Listen
Dezső Mező
Founder, DField Solutions
ShareXLinkedIn#
React Native offline-first: SQLite + sync that works

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.

Layer 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'
);

Layer 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.

Layer 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`.

Layer 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]'.

Background 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));
  }
});

Edge 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.

ShareXLinkedIn#
Dezső Mező
By

Dezső Mező

Founder, DField Solutions

I'm a full-stack engineer and I build across the whole stack myself · AI agents, web and mobile apps, blockchain, backends, security, right down to the OS layer. If it's software, I've probably built it and broken it.

Keep reading
RELATED PROJECTS
Let's talk

Would rather build together?

Let's talk about your project. 30 minutes, no strings.