Zurück zum Blog
Mobile App

How to Build an Offline-First Mobile App

How do you build a mobile app that works offline? Offline-first architecture, an on-device database, an operation queue, sync and conflict rules, authentication and the scenarios you must test.

OfflineMobil UygulamaSenkronizasyonReact Native

An offline mobile app is one where users can enter data and view records without a connection, and everything they did is sent to the server without loss once the connection returns. The way to build it is to invert the "show an error when the connection drops" logic: the app writes to a local database on the device first, keeps changes in a queue and syncs in the background. This architecture is called offline-first, and the hard part is not storing the data but deciding up front which change wins when two devices edit the same record.

Which apps need to work offline?

Not every app needs to work offline, and building it when it is not needed raises cost considerably. The rule is simple: if users do their job somewhere without a connection and data they cannot save at that moment gets lost, offline is mandatory. Field teams, warehouse and construction-site staff, drivers, inspectors and sales reps in rural areas belong in this group; we covered this need separately for construction companies, logistics and field service software. On the other hand, in an app that shows live prices, stock or balances, showing stale data leads to wrong decisions; there the right behaviour is to show data with its "last updated" time and require a connection for the action.

  • Full offline: users can work all day without a connection, creating and editing records (field forms, delivery records, stock counts).
  • Read-only offline: users can see the last downloaded data but cannot change it (catalogue, customer list, documents).
  • Partial offline: some actions are queued, others (payment, booking, stock deduction) need a connection, and the interface makes the difference clear.

The three parts of an offline-first architecture

The first part is the local database on the device. Screens read from this database, not from the server; server data is synced into it. SQLite-based solutions are the most common choice on both React Native and Flutter. Simple key-value storage is not enough for data that is filtered and related. The second part is the operation queue: every change the user makes is written to the local database and, at the same time, recorded as an operation "to be sent to the server". The third part is the sync engine: when a connection is available it sends the queue in order, pulls new changes from the server and resolves conflicts according to the rules.

type PendingOp = {
  id: string; // unique ID generated on the client (idempotency key)
  entity: 'visit' | 'photo' | 'signature';
  payload: unknown;
  createdAt: number;
  attempts: number;
};

export async function flushQueue(db: LocalDb, api: Api): Promise<void> {
  const ops = await db.pendingOps.orderBy('createdAt').toArray();
  for (const op of ops) {
    try {
      // The server accepts a second request with the same id without creating a new record
      await api.post('/sync/' + op.entity, { opId: op.id, data: op.payload });
      await db.pendingOps.delete(op.id);
    } catch (err) {
      if (isConflict(err)) {
        await db.conflicts.add({ op, server: err.serverVersion });
        await db.pendingOps.delete(op.id);
        continue;
      }
      await db.pendingOps.update(op.id, { attempts: op.attempts + 1 });
      break; // Network error: stop to keep the order, retry later
    }
  }
}

Three decisions in the example are permanent. The ID is generated on the client; if the connection drops after a request is sent, the app sends the same operation again and the server does not create a second record. On a network error the queue stops, because sending "add photo to visit" while "create visit" has failed creates an orphaned record on the server. A conflict is not silently overwritten; it is shown to the user or a manager in a separate list.

In an offline app the real design decision is not where you store the data but which change wins when two devices edit the same record; that rule cannot be bolted on later.

Conflict rules: last write wins is not enough

The easiest rule is "the last change wins", and in most projects it gives the wrong result. On two devices with different clocks "last" is unreliable, and overwriting the whole record also wipes the fields the other user changed. The right approach is to resolve conflicts at field level and by record type.

  • Append-only records (visit notes, photos, signatures) do not conflict; all of them are kept.
  • When different fields changed, both changes are merged; when the same field changed, the record goes to the conflict list.
  • Counters and quantities are sent not as a "new value" but as an increment or decrement, so two devices’ counts do not overwrite each other.
  • For status fields the server rule applies: a job cancelled on the server cannot be reopened as "completed" from a device in the field; it is reported as a conflict.
  • Deletion is done with a "deleted" flag rather than physically removing the record; otherwise an offline device brings the deleted record back on its next sync.

Authentication, security and storage limits

In an offline app the session cannot be refreshed when the token expires, because there is no connection. So the maximum time a user can work offline is set as a business decision, and data stored on the device during that time is encrypted. Tokens and the encryption key are kept in the operating system’s secure store rather than plain storage; we covered the details in our mobile app security article. The data downloaded to the device is limited too: users only get data for their own region and their own jobs. Downloading the full customer list to every device fills storage and turns a lost phone into a data leak; which data reaches whom should come from the same rules as your roles and permissions setup.

Scenarios you must test

An offline feature always works when tested on fast office Wi-Fi. Real problems show up on weak, intermittent connections, when the app is sent to the background and on devices that have not synced for days. The test plan should include at least: the connection dropping while a request is in flight, the app being closed and reopened with a full queue, two devices editing the same record offline, a device with the wrong clock, a device that stays offline for a long time and builds up many operations, and the app being updated while operations are still queued. The last one matters most: if the new version changes the local database schema, operations queued in the old version must be migrated without loss.

Impact on cost

Offline-first takes noticeably more work than an online app with the same screens, because every data type needs a local schema, a sync endpoint and a conflict rule, and the test matrix grows. That is why the quote should list, screen by screen, what truly works offline; "everything should work offline" is usually unnecessary scope growth. This distinction is one of the items that most changes mobile app development cost, and adding it later costs more than building it in from the start, because the data source of existing screens changes.

Conclusion

An offline mobile app is built with a local database on the device, an ordered and safely retryable operation queue, and conflict rules written per record type. Decide up front which screens work offline, how long users can stay disconnected and which data reaches the device, and you get a product that is reliable in the field and never loses data. If you are planning an offline app for your field team, take a look at our mobile app service or request a quote.

Let's Build Your Project

Get a free consultation for your website, mobile app, or corporate software project.

Get a Free QuoteExplore our Mobile App service