---
term: 'Offline-First Data Synchronization in Mobile Backends'
seoTitle: 'Offline-First Data Sync: Local Stores, Conflicts, and Replay'
headline: 'What is Offline-First Data Synchronization?'
slug: offline-first-data-sync
category: api-realtime
shortDefinition: 'Offline-first sync is an architecture where the app reads and writes a local store first, then reconciles with the server when online.'
relatedTerms:
  - real-time-live-queries
  - websockets-real-time-sync
  - push-notifications-apns-fcm
  - cross-platform-development
contrastsWith:
  - real-time-live-queries
faq:
  - question: 'What is an offline-first app?'
    answer: 'An app whose screens read and write a database on the device, with the network demoted to a synchronization detail. Every tap works in airplane mode because nothing on the interaction path waits on a server; a background sync layer reconciles the local store with the backend whenever connectivity allows. Contrast online-first apps, which render spinners until the server answers.'
  - question: 'How does offline data synchronization work?'
    answer: 'Two loops. Outbound: local writes land in the device store and a durable outbox queue; when the network returns, the queue replays against the server in order. Inbound: the client pulls changes since its last sync — by timestamp, sequence number, or change feed — and applies them locally. Conflict resolution sits where the loops meet, deciding what happens when both sides edited the same record.'
  - question: 'What is last-write-wins and when is it safe?'
    answer: 'The simplest conflict policy: compare timestamps or versions and keep the newest write, discarding the other silently. It is safe when edits are whole-record and low-stakes — a settings toggle, a status flag — or when one writer per record is the norm. It is dangerous for shared documents and counters, where "discard the other edit" means losing someone''s work.'
  - question: 'How do CRDTs resolve conflicts?'
    answer: 'By making conflicts unrepresentable: a conflict-free replicated data type constrains each field to operations that merge deterministically regardless of arrival order — grow-only counters, add/remove sets, last-writer registers. Two replicas that exchange states always converge without coordination. The cost is modeling discipline: your data must be expressed in CRDT vocabulary, and full CRDT libraries are heavyweight for typical form-and-list mobile apps.'
  - question: 'What happens to writes made while offline?'
    answer: 'They queue. A durable outbox records each pending operation, and the local store reflects the write immediately so the interface stays truthful to the user''s intent. On reconnect, the queue replays in order; failures need policy — retry transient errors, surface permission and validation rejections back into the interface rather than retrying forever. Idempotent operations make replay safe when acknowledgments get lost.'
  - question: 'Is offline-first the same as caching?'
    answer: 'No — direction is the difference. A cache accelerates reads: the server stays the source of truth and stale entries are merely refetched. Offline-first also accepts writes locally, which is what creates divergence between device and server and forces the hard machinery — outbox queues, replay, conflict resolution. Read caching is a performance optimization; offline-first is a data-architecture commitment.'
  - question: 'When should you not build offline-first?'
    answer: 'When correctness depends on a single authoritative state: payments, bookings, inventory decrements, anything where two devices acting on stale data creates real-world double-spends. Also when data is server-computed and read-mostly — a news feed degrades gracefully with a plain cache. The sync machinery is a permanent tax; pay it where field conditions or user expectations demand it.'
  - question: 'How do mobile SDKs pin data locally?'
    answer: 'Pinning marks objects for durable device storage: pinned records survive restarts, are queryable offline through the same query interface as the server, and stay linked to their server identities so later syncs update them in place. Paired with queued writes — save-eventually semantics — pinning gives a working offline layer without hand-rolling a database schema on the device.'
codeLanguages: [javascript, dart, swift, kotlin]
externalAuthorities:
  - name: 'Conflict-free replicated data type (Wikipedia)'
    url: 'https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type'
  - name: 'Eventual consistency (Wikipedia)'
    url: 'https://en.wikipedia.org/wiki/Eventual_consistency'
  - name: 'Version Vector — Patterns of Distributed Systems (martinfowler.com)'
    url: 'https://martinfowler.com/articles/patterns-of-distributed-systems/version-vector.html'
  - name: 'Local Datastore — Parse iOS SDK guide'
    url: 'https://docs.parseplatform.org/ios/guide/#local-datastore'
cta:
  title: 'Ship apps that work in airplane mode'
  text: 'Back4app''s mobile SDKs include a Local Datastore out of the box — pin queries for offline reads, queue writes with save-eventually, and let the managed backend be the source of truth your devices reconcile against.'
  linkText: 'Start building free'
  linkUrl: 'https://www.back4app.com/signup'
author: 'Back4app Engineering'
publishedDate: '2026-08-05'
translationKey: offline-first-data-sync
---

**Offline-first sync is an architecture where the app reads and writes a local store first, then reconciles with the server when online.** The network stops being a dependency and becomes a background process — every screen renders from the device, every tap commits to the device, and a sync layer settles accounts with the backend whenever connectivity permits. The hard part was never storing data locally; it's what happens when two copies of the truth disagree.

## Key takeaways

| Question | Answer |
| --- | --- |
| The core move | Screens talk to a device-local store; the network syncs in the background |
| The two postures | Local store as *buffer* (server owns truth) or as *replica* (truth is negotiated) |
| Writes offline | Durable outbox queue → replay in order on reconnect |
| Conflicts | Last-write-wins · field-level merge · CRDTs — pick per field, not per app |
| The honest cost | Sync machinery is permanent complexity — spend it where offline matters |

## Pin, read, queue: the offline loop in code

The mobile SDK version of the pattern — pin what screens need, queue what users change:

**JavaScript:**

```javascript
// JavaScript — Back4app JS SDK with the Local Datastore enabled
Parse.enableLocalDatastore();

// Read: serve the screen from the local store — instant, works offline
const query = new Parse.Query('Task');
query.fromLocalDatastore();
render(await query.find());

// Write: durable locally now, queued for the server automatically
const task = new Parse.Object('Task');
task.set('title', 'Inspect site 14');
task.set('done', false);
await task.pin();          // survives restarts, feeds local reads
task.saveEventually();     // replays when the network returns
```

**Flutter:**

```dart
// Flutter / Dart — Back4app Flutter SDK with local storage enabled
// Parse().initialize(..., coreStore: await CoreStoreSembastImp.getInstance())

// Read: serve the screen from pinned data — instant, works offline
final pinned = await ParseObject('Task').fromPin('openTasks');
render(pinned);

// Write: durable locally now, synced when the network allows
final task = ParseObject('Task')
  ..set('title', 'Inspect site 14')
  ..set('done', false);
await task.pin();                 // survives restarts, feeds local reads

final response = await task.save();
if (!response.success) retryLater(task);   // replay from the outbox
```

**Swift:**

```swift
// iOS / Swift — Back4app iOS SDK with the Local Datastore enabled
// (in the app delegate) Parse.enableLocalDatastore()

// Read: serve the screen from the local store — instant, works offline
let query = PFQuery(className: "Task")
query.fromLocalDatastore()
query.findObjectsInBackground { tasks, _ in
  render(tasks)
}

// Write: durable locally now, queued for the server automatically
let task = PFObject(className: "Task")
task["title"] = "Inspect site 14"
task["done"] = false
task.pinInBackground()     // survives restarts, feeds local reads
task.saveEventually()      // replays when the network returns
```

**Kotlin:**

```kotlin
// Android / Kotlin — Back4app Android SDK with the Local Datastore enabled
// (before Parse.initialize) Parse.enableLocalDatastore(context)

// Read: serve the screen from the local store — instant, works offline
val query = ParseQuery.getQuery<ParseObject>("Task")
query.fromLocalDatastore()
query.findInBackground { tasks, _ ->
    render(tasks)
}

// Write: durable locally now, queued for the server automatically
val task = ParseObject("Task")
task.put("title", "Inspect site 14")
task.put("done", false)
task.pinInBackground()     // survives restarts, feeds local reads
task.saveEventually()      // replays when the network returns
```

Two calls carry the architecture. *Pin* makes objects durable and queryable on the device — the same query interface as the server, pointed at local storage ([Local Datastore](https://docs.parseplatform.org/ios/guide/#local-datastore) in the SDKs above). *Save-eventually* accepts the write now and owns its delivery later. Everything else in this article is what happens between those two calls.

## Buffer or source of truth? The decision under the decision

Every offline-first design quietly picks one of two postures for the local store, and most sync pain comes from not picking deliberately.

**Local store as buffer.** The server remains authoritative; the device holds a working copy plus an outbox of intent. Conflicts resolve in the server's favor or by simple policy, and a device can always be repaired by re-pulling. This is the right default for business apps — CRM checklists, field inspections, order entry — because reasoning stays simple: the truth lives in one place, devices just lag it.

**Local store as replica.** Truth is *negotiated* among peers and the server is one replica among several — the posture of collaborative editors and note apps that promise merge-anything semantics. This buys resilience and user trust at the price of real distributed-systems machinery: version vectors to detect concurrency, merge functions per data type, and [eventual consistency](https://en.wikipedia.org/wiki/Eventual_consistency) as the strongest guarantee you can honestly print.

The posture is chosen per *dataset*, not per app: the same field-service app can treat the technician's own work orders as a replica (they must be editable all day underground) and the parts catalog as a read-through buffer.

## The sync loop, end to end

```mermaid
flowchart LR
  accTitle: Offline-first synchronization loop
  accDescr: The app reads and writes a local store. Writes also enter a durable outbox queue. When connectivity returns, the queue replays ordered operations to the server, the server applies conflict resolution against concurrent edits, and the client pulls changes since its last sync into the local store.
  UI["App screens"] -->|"read + write"| LS[("Local store")]
  UI -->|"each write"| OB["Outbox queue<br/>(durable, ordered)"]
  OB -->|"replay on reconnect"| SV["Server<br/>(conflict resolution)"]
  SV -->|"changes since last sync"| LS
  SV --- DB[("Backend database")]
```

The loop has an outbound half and an inbound half, and they meet at conflict resolution.

**Outbound: queued writes and replay.** Offline writes commit locally and append to a durable outbox — the interface reflects intent immediately, honestly labeled "pending" where it matters. On reconnect the queue replays in order. The engineering is in the failure modes: transient errors retry with backoff; permission and validation rejections must *surface to the user*, not retry forever; and operations should be idempotent, because a lost acknowledgment means the same operation may arrive twice. Replaying "set status to done" twice is harmless; replaying "increment stock by 3" twice is a bug.

**Inbound: delta pull.** Refetching everything on reconnect wastes bandwidth and battery; production sync pulls *changes since a checkpoint* — an updated-at watermark in simple designs, a server sequence number or change feed in stronger ones. The same events that power [live queries](/glossary/real-time-live-queries/) when the app is online double as the inbound delta stream, which is why offline sync and real-time sync are one continuum, not two features.

## Last-write-wins vs. merge vs. CRDTs

| Strategy | How it resolves | Loses data? | Cost | Right for |
| --- | --- | --- | --- | --- |
| Last-write-wins | Newest timestamp/version keeps the record | **Yes — silently** | Trivial | Single-writer records, toggles, statuses |
| Field-level merge | Non-overlapping field edits both survive; same-field conflicts get policy | Only on same-field collisions | Moderate | Forms and records edited by few people |
| Manual resolution | Both versions kept; a human chooses | No | Interface + workflow | High-stakes records, sync consoles |
| CRDT / CRDT-lite | Data types whose operations merge deterministically | No | Modeling discipline, library weight | Counters, sets, collaborative structures |

Two honest notes on this table. First, timestamps are shakier than they look — device clocks skew, so serious last-write-wins uses server receipt time or logical versions ([version vectors](https://martinfowler.com/articles/patterns-of-distributed-systems/version-vector.html) when concurrent edits must be *detected* rather than guessed). Second, the practical sweet spot for mobile apps is **CRDT-lite**: full [CRDT](https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type) libraries are heavy machinery, but stealing the per-field idea is cheap — model a quantity as *increment operations* instead of a stored total, tags as add/remove *sets* instead of an array, and those fields simply stop conflicting. Strategy is chosen per field; apps that pick one global policy usually picked wrong for some field.

## Common use cases

- **Field operations** — inspections, deliveries, utilities work: the job site is precisely where coverage dies, so capture must be local and sync opportunistic.
- **Note-taking and personal productivity** — single user, multiple devices: conflicts are rare and mostly self-inflicted, making merge policies tractable.
- **Point-of-sale and kiosk apps** — the queue must keep moving through a router reboot; transactions replay when the link returns.
- **Travel-context apps** — itineraries, maps, tickets consumed exactly where connectivity is worst: planes, trains, roaming.
- **Emerging-market and low-bandwidth products** — intermittent, metered connections make delta-sync-in-background the only respectful design, whatever the [platform mix](/glossary/cross-platform-development/).

## Should you build offline-first? A decision matrix

| Build offline-first when… | Stay online-first when… |
| --- | --- |
| Users work where coverage fails — field, transit, basements | Users are effectively always connected (desktop web, office tools) |
| A blocked tap costs money or trust (POS, capture-in-the-field) | Correctness needs one authoritative state *now* — payments, bookings, inventory |
| Data is user-owned records with few concurrent writers | Data is server-computed or shared-hot (feeds, dashboards) — cache reads, keep writes online |
| Writes tolerate later reconciliation | Two devices acting on stale truth is a real-world hazard |
| You can fund the sync layer's permanent upkeep | A loading state is honestly good enough |

The middle path is legitimate and common: cache reads for instant screens, allow offline writes only for the few datasets that truly need them, and keep the rest online-first behind honest connectivity states.

## Limitations and trade-offs

- **Conflict resolution is a product decision wearing an engineering costume.** "Which edit survives?" has no technically correct answer — someone must own the policy per field, and "silently keep the newest" is a choice with a victim.
- **The outbox is a liability queue.** Stale pending writes from a device that reappears after two weeks can replay into a changed world; expiry policies and validation-on-replay are mandatory, not paranoid.
- **Sync bugs are the worst bug class you can buy.** They reproduce only under specific interleavings of edits and connectivity — invest in deterministic replay tests and a visible sync-status surface, or debug by anecdote forever.
- **Local truth ages.** Screens must be honest about staleness where it matters (pricing, availability) — "as of 2 hours ago" is a feature, not an apology.
- **Storage and identity add drag.** Device stores need migrations like any database, and offline-created records need client-generated identities that survive the trip to the server without duplicating.

## Offline-first sync on Back4app

Back4app is an open-source Backend-as-a-Service (BaaS) platform that combines a managed database, auto-generated REST and GraphQL APIs, authentication, file storage, and Cloud Code serverless functions. Its mobile SDKs ship the buffer posture ready-made: the Local Datastore pins any query's results for offline reads through the same query API, `saveEventually` gives every write a durable outbox with ordered replay, and Cloud Code `beforeSave` triggers are the natural home for server-side conflict policy — version checks, merge rules, replay validation — enforced on every write path. Pair the [Live Query](/glossary/real-time-live-queries/) stream for inbound deltas while the app is online, and the sync loop in the diagram above is configuration plus policy, not a subsystem you build.
