---
title: "React Native offline-first: SQLite + sync that works"
description: "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."
date: 2026-04-22
updated: 2026-04-22
author: "Dezső Mező"
tags: "Mobile, React Native, Offline-first, SQLite, Sync, mobile"
slug: react-native-offline-first-sqlite-sync
canonical: https://dfieldsolutions.com/blog/react-native-offline-first-sqlite-sync
---

# 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.
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).

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

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

> **TIP:** 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.

---

Source: https://dfieldsolutions.com/blog/react-native-offline-first-sqlite-sync
Author: Dezső Mező · Founder, DField Solutions
Site: https://dfieldsolutions.com
