TechnicalApril 2026 · 12 min read

Building Offline-First React Native Apps with AI

An app that requires a network is an app users abandon in tunnels, elevators, and airplanes. Offline-first is a design stance: local is the source of truth, sync is an optimization. This guide covers the 2026 stack for offline-first React Native apps in Expo — storage, sync, conflicts, and how to get AI builders to generate the right patterns.

Core principle

Local storage is the source of truth. Every user action writes locally first and succeeds immediately. Sync to the server happens in the background when online. The UI never shows a spinner for writes.

Local storage options for Expo in 2026

LibraryBest forNotes
MMKVSettings, session, small objectsFastest, synchronous reads, small API
AsyncStorageSimple key-valueWorks, slower, legacy fallback
SQLite (expo-sqlite, op-sqlite)Relational dataUse with Drizzle ORM for ergonomics
WatermelonDBLarge relational datasetsBuilt-in sync primitives
PowerSyncReal-time sync with PostgresCommercial, excellent DX
RealmObjects + syncMongo-owned, robust

The recommended 2026 stack for most apps

  • MMKV for small/fast data — current user, settings, session tokens.
  • SQLite via Drizzle for relational data — tasks, posts, orders.
  • Supabase as the remote backend. Use @tanstack/react-query or a custom sync layer to mirror tables locally.
  • NetInfo from @react-native-community/netinfo to detect connectivity.

This covers 90% of consumer and prosumer apps. For collaborative or real-time products (shared docs, live cursors), consider PowerSync or a CRDT-backed stack.

Sync strategies: picking the right one

  • Optimistic UI + background sync: writes land locally instantly; a queued job syncs to the server. Simple, works for most apps.
  • Last-write-wins (LWW): server timestamp decides conflicts. Good default; loses data only in edge cases of simultaneous multi-device edits.
  • CRDT: conflict-free replicated data types. For collaborative editing or shared documents. Heavy to implement; libraries like Yjs and Automerge help.
  • Queue-based sync: every local mutation goes to a queue table. When online, the queue drains in order. Retry on failure. Simple, robust, easy to debug.

A minimal queue-based sync implementation

// Local write:
async function createTask(task) {
  const id = uuid();
  // 1. Write to local SQLite immediately
  await db.insert(tasks).values({ id, ...task,
    synced: false });
  // 2. Enqueue for sync
  await db.insert(sync_queue).values({
    action: 'insert',
    table: 'tasks',
    payload: { id, ...task },
    created_at: Date.now(),
  });
  // 3. UI updates instantly
  return id;
}

// Sync drain:
import NetInfo from
  '@react-native-community/netinfo';

NetInfo.addEventListener(async (state) => {
  if (state.isConnected) await drainQueue();
});

async function drainQueue() {
  const items = await db.select().from(sync_queue)
    .orderBy(sync_queue.created_at);
  for (const item of items) {
    try {
      await syncToSupabase(item);
      await db.delete(sync_queue)
        .where(eq(sync_queue.id, item.id));
      await db.update(tasks)
        .set({ synced: true })
        .where(eq(tasks.id, item.payload.id));
    } catch (e) {
      // leave in queue, retry next time online
      break;
    }
  }
}

Conflict resolution in practice

Two devices edit the same task offline. On sync, whose wins? The simplest pattern:

  • Store updated_at on every row (both local and server).
  • On sync, compare timestamps. Newer wins. Server is authoritative on ties.
  • Keep a sync_conflicts table for cases where the user should see both versions (rare, worth showing).
  • For delete conflicts, use soft deletes (deleted_at timestamp) and sync that. Hard deletes are ambiguous offline.

Detecting and responding to connectivity

  • Use NetInfo.addEventListener to react to connectivity changes.
  • Show a subtle “Offline” indicator in the UI — not a blocking modal.
  • Distinguish “no internet” from “internet but server unreachable” — both land in queue, but showing different copy builds trust.
  • Always let writes succeed locally. Never block UI on connectivity.

Getting AI builders to generate offline-first code

AI app builders default to network-required patterns unless asked. Anchor words that steer toward offline-first:

  • “Offline-first,” “optimistic UI,” “local is source of truth”
  • “MMKV” and “SQLite via Drizzle”
  • “Sync queue that drains when online”
  • “No spinners on writes”
  • “Last-write-wins conflict resolution”

Apps generated by ShipNative with those anchors produce the sync queue + local storage scaffolding for you. See the broader prompt playbook in How to Write Prompts That Produce Better React Native Code.

Testing offline flows

  • Toggle airplane mode on a physical device (not a simulator).
  • Create, edit, delete records while offline. Verify UI succeeds instantly.
  • Reconnect and verify the queue drains.
  • Edit the same record on two devices while both offline, then reconnect both. Verify conflict resolution behaves as expected.
  • Kill the app mid-sync to confirm queue items persist and retry on next launch.

Common pitfalls

  • Blocking writes on network. Writes should always land locally first.
  • No retry logic. Transient network failures must not lose data.
  • Hard deletes without soft-delete markers — creates sync ambiguity.
  • Spinners on writes. Offline-first means instant UI.
  • Forgetting offline testing. Simulators lie about network behavior.

Frequently Asked Questions

Does every app need to be offline-first?

No, but more do than realize. If users open your app on the subway, in elevators, on planes, or in areas with spotty Wi-Fi, offline-first pays for itself. Productivity, journal, fitness, reading, and note apps should default to offline-first. Purely social or live-data apps (real-time chat with no local cache) can skip it.

What is the simplest offline stack for Expo?

MMKV for key-value (settings, session, small objects) + SQLite via expo-sqlite or op-sqlite for relational data. Mirror Supabase tables locally, sync on connectivity change. Avoid rolling your own if possible — use an ORM like Drizzle or a library like WatermelonDB that handles the sync plumbing.

How do I handle conflicts when two devices edit the same data offline?

For most apps, last-write-wins (LWW) based on server timestamp is fine. More advanced: use CRDTs or operational transforms for collaborative editing. Solo-user apps with occasional multi-device use rarely need CRDTs — LWW covers 95% of cases.

Can AI app builders generate offline-first code?

Yes, if you ask for it in the prompt. Name "MMKV," "offline-first," "optimistic UI," and "sync when online." Without those keywords, AI builders default to network-required patterns. The code is not fundamentally harder — the prompt just has to request it.

How do I test offline flows?

Toggle airplane mode on a physical device. Test: create, edit, delete while offline; reconnect and verify sync. Use the React Native Debugger with Network tab to verify no unexpected calls. Simulators have limited offline emulation — always validate on real devices.

Supabase vs Firebase 2026

The remote backend behind most offline-first apps.

Read guide →

Build a Productivity App with AI

Offline-first is non-negotiable for productivity.

See vertical guide →

Ship a real React Native app today

Describe, preview, and export Expo code — free to start.

Build with ShipNative →