Skip to content
BuildThenTestedSee packages
HighMobile

Offline state corrupting data

Users lose edits they watched themselves save, records end up half-updated, and nobody can tell which copy of the data is the real one. Risks specific to an app installed on a phone. Check it yourself with the steps below; our test adds what a script and a second account can find.

What it looks like

// On reconnect, push whatever is cached.
for (const item of pendingEdits) {
  await supabase.from("orders").upsert(item);   // no version, no timestamp.
}
// Elsewhere: two screens each write the full record.
await db.runAsync("UPDATE orders SET status = ?, total = ?", status, total);
A sync routine that replays cached edits with no versioning and no transaction, so the last device to reconnect overwrites everyone else.

What happens

Users lose edits they watched themselves save, records end up half-updated, and nobody can tell which copy of the data is the real one.

Where it shows up

Descriptive, not a verdict: builders and stacks whose defaults make this reachable, or that a public incident names. Each hub states the tool's own security features.

Check it yourself

Uses your builder's dashboard and your browser. No tools to install.

  1. On two phones (or a phone and the simulator) signed into the same account, put one in airplane mode and edit the same record on both.
  2. Change a second field on the online device, then reconnect the offline one and wait for sync.
  3. Open the record on both devices and in your database dashboard and compare every field.
  4. A bad result is a field silently reverting, the two devices showing different data, or a record that shows half of each edit.

What we check

We drive the app through scripted interruptions: killing it mid-save, dropping the connection during sync, and replaying the same queued edit twice. The report shows each resulting database state next to what the user saw on screen.

Fix direction

Give every record an updated-at or version field and make the server reject or merge stale writes. Wrap multi-step local writes in an exclusive transaction, and make queued edits idempotent (safe to replay) so a second delivery does no harm. Sync libraries such as WatermelonDB expose a conflict resolver and per-column change tracking; Expo SQLite provides exclusive transactions.

Mechanism documented in: Expo Docs: SQLite (transactions) (verified 2026-09-21); WatermelonDB Docs: Synchronization, frontend (verified 2026-09-21).

Related patterns

  • Race conditions on counters and balancesData integrity

    Users can spend the same credits twice, book the last seat twice, or redeem a coupon more times than allowed, and the stored totals stop matching what was actually given out.

    High
  • No idempotency on payment handlersPayments

    A customer is charged twice after a network hiccup, or a replayed webhook tops up an account again and again for a single payment.

    High
  • Retries with no pause and no capReliability

    One outage turns into a flood of retries from every open browser tab, which keeps the service down longer, runs up usage bills and can submit the same order or payment twice.

    Medium
  • Deletes with no soft delete or audit trailData integrity

    One wrong click, a bug in a filter, or an abused account removes customer data for good, and nobody can say afterwards what was lost or who removed it.

    High

Questions

why does my app lose changes after going offline

The usual cause is a sync routine that replays cached edits without a version or timestamp, so a device that reconnects later overwrites edits made in between. Add an updated-at field, have the server compare it before writing, and test with two devices editing the same record while one is in airplane mode.

how do i test offline sync in a react native app

Use two signed-in devices, edit the same record on both with one offline, reconnect and compare each field on both screens and in the database. Then kill the app during a save and reopen it. Any reverted field or half-written record is a finding to fix before launch.

what is a conflict resolver in watermelondb

WatermelonDB's synchronize function pulls all changes since the last sync and pushes local ones, and accepts a conflictResolver option that decides how a record is updated when it changed in both places. Without one, you rely on the default merge and should test that it matches what your users expect.

Back to the catalogue, or run the self-check for your stack.