Skip to content
khua
← All writing

Article · July 11, 2026

Offline-First React Native with Supabase: What Actually Works

Most offline tutorials cache reads and stop. That is the easy half. Here is how writes, conflicts and a queue that survives a force-quit actually go.By Khua · 2 min read

"Offline-first" usually means "we cached the list". That is the easy half, and it is not the half that generates support tickets.

The hard half is writes: what happens when someone edits a record on a train, force-quits the app, and reopens it two days later on a different network.

The shape that works

Three pieces, and they have to be separate:

  1. A local database that is the source of truth for the UI. The screen reads from local storage, always. Never from the network directly. This is the single decision that makes everything else possible — the UI never has a loading state tied to connectivity.
  2. A durable outbox. Every write appends an intent to a persisted queue before it touches the UI. Not a state array; something that survives a process kill.
  3. A reconciler. On reconnect, it drains the outbox in order, applies server responses back to local storage, and resolves conflicts by an explicit rule.

If any of the three lives in React state, you will lose writes and you will not find out until a user tells you.

Conflict resolution, decided up front

Last-write-wins is the default and it is usually wrong, because it silently discards work. Pick per-table, and write the rule down:

  • Append-only data (messages, log entries, readings): no conflict possible. Just replay.
  • User-owned records (a profile, a draft): last-write-wins is fine, because there is one writer.
  • Shared records (a team's shared document, inventory counts): needs a merge or an explicit prompt. Do not paper over this one.

Supabase does not decide this for you, and no library will either. It is a product question wearing an engineering costume.

Where Supabase helps and where it does not

Helps: Postgres means real constraints, so a bad merge fails loudly at the database rather than corrupting quietly. Row-level security means the reconciler cannot be tricked into writing someone else's row even if the client is compromised. Realtime gives you invalidation without polling.

Does not help: there is no built-in sync engine. The outbox and the reconciler are yours to write. Anyone telling you otherwise has built a demo, not an app.

The test that matters

Airplane mode, make five edits, force-quit from the app switcher, reopen, come back online. Every edit should land, in order, exactly once.

Almost no offline implementation passes that on the first try. Mine did not.


I have built this loop for a real product. The case study is here, and if you are staring down the same problem, say hello.