Skip to content
Innovialabs

Offline-First Mobile Apps: Local Storage and Data Sync

Advanced4 min

In short

In an offline-first app the UI reads and writes only to a local database, while sync with the server runs in the background through a queue of changes. The hard part is not storage but resolving conflicts and retrying without duplicates.

What offline-first means

Offline-first is an architecture where the source of truth for the UI is a local database on the device. Screens read and write locally and never wait for the network. A separate sync layer pushes changes to the server and pulls new ones in the background.

It pays off when people work with poor connectivity: couriers, sales reps, field staff, warehouses, notes and task apps. For a catalog that simply displays server data, a cache is usually enough.

Choosing a local database

OptionPlatformWhen it fits
SQLiteEverywhereFull control over schema and queries; via wrappers such as GRDB, SQLDelight, drift, sqflite
RoomAndroidThe Android standard: SQLite with compile-time query checks and migrations
Core Data / SwiftDataiOSApple's native stack, integrated with SwiftUI and CloudKit
RealmiOS, Android, Flutter, React NativeObject model without SQL. MongoDB announced the deprecation of Atlas Device Sync, so you will need to build sync for Realm yourself

Flutter teams often choose drift (built on SQLite); React Native teams use SQLite wrappers or WatermelonDB.

Whatever the database:

  • plan schema migrations from the first release — users will have old data versions;
  • store a server ID, local ID, modification time and sync status on every record;
  • use soft deletes (tombstones), otherwise the server never learns a record was deleted offline.

Sync strategies

Pulling changes:

  • Delta sync: the client sends a cursor or last-sync marker, and the server returns only what changed since. This is the main approach.
  • Full reload: for small reference data, or as a fallback when local data is corrupted.
  • Push notification as a signal: the server says "there are changes", and the app pulls.

Pushing changes goes through an outbox: every change is saved to the database together with an outbox entry, and a background worker sends the queue in order.

CREATE TABLE outbox (
  id TEXT PRIMARY KEY,        -- idempotency key (UUID)
  entity TEXT NOT NULL,
  operation TEXT NOT NULL,    -- create | update | delete
  payload TEXT NOT NULL,      -- JSON
  attempts INTEGER NOT NULL DEFAULT 0,
  created_at INTEGER NOT NULL
);

A request queue without duplicates

  • Write the data and the outbox entry in one transaction.
  • Every request carries an idempotency key: if a response is lost and the client retries, the server does not create a duplicate.
  • Retry with exponential backoff and an attempt limit; after the limit, tell the user the change was not sent.
  • Send in the background with WorkManager on Android and BGTaskScheduler on iOS. The system decides when tasks run, so also flush the queue when the app opens.
  • Respect dependencies: an order cannot be sent before a customer created offline. Send in order, or send local IDs and map them on the server.

Conflict resolution

A conflict happens when the same record is changed on two devices before sync.

StrategyHow it worksRisk
Last-write-winsThe latest change by time winsSilent data loss; device clocks can be wrong
Record versionsThe client sends a version, the server rejects stale onesYou must decide what happens after a rejection
Field-level mergeChanged fields are combined; conflict only when the same field changedHarder to implement
CRDTData structures that merge without conflictsComplex; suited to collaborative editing
User decidesShow both versionsGood for important documents, tiring when conflicts are frequent

For most business apps a working setup is record versions plus field-level merge, with last-write-wins only for unimportant data. Use server time for comparisons.

Testing poor network conditions

  • Network Link Conditioner on iOS and the Android emulator's network settings to simulate slow connections and packet loss.
  • Proxies such as Charles or Proxyman to throttle speed and rewrite responses.
  • Manual scenarios: airplane mode mid-upload, killing the app with a non-empty queue, two devices editing one record, updating the app with unsynced data.
  • Automated tests for the sync layer with a fake network that returns errors, times out or duplicates responses.

FAQ

No. It adds real complexity to the app and the backend. Do it when working without a network is a genuine user scenario; otherwise a cache and clear error messages are enough.

We can help with this

If you'd rather get the result than figure it all out yourself — this is what we do every day.

Discuss a project

Read next

Have an idea or a task? Tell us about it

We'll reply within one business day and propose a solution. The consultation is free.