Offline-first is often described as “save locally and sync later.” That definition is fine for a notes app. It is dangerously incomplete for ERP, POS, inventory, invoicing, and field-sales software.
In business systems, offline-first is really a consistency problem.
The application must keep working without a network while preventing duplicate invoices, lost stock movements, broken payment states, and conflicting edits when connectivity returns.
Local data is not a cache
For critical workflows, local storage is part of the system of record during the offline period.
That means the local model needs enough information to answer questions like:
- Was this transaction created locally or received from the server?
- Has it been submitted?
- Was submission acknowledged?
- Can it be retried safely?
- Did the server reject it?
- Has another device already changed the same record?
A simple isSynced boolean usually stops being enough very quickly.
I prefer explicit states such as:
draft -> pendingSync -> syncing -> synced
with separate failure states for retryable and permanent errors.
Every mutation needs a stable client identity
If a user taps “Submit Invoice,” loses the connection, and the app retries later, the backend must be able to recognize the same logical operation.
A client-generated UUID is one of the most useful tools here.
The request can be retried multiple times while the server enforces idempotency against that stable identifier. Without this, a retry can easily become a duplicate sale or duplicate payment.
The queue should store intent, not random HTTP requests
A strong offline queue records a business action with enough context to replay it safely.
For example:
- Create invoice.
- Add payment receipt.
- Submit stock transfer.
- Register customer visit.
- Upload damaged inventory record.
This is better than storing arbitrary URLs and JSON blobs because the queue remains understandable and testable as the API evolves.
Conflict strategy must be decided per domain
There is no universal “last write wins” rule for business software.
A product description can often accept a simple latest-update policy. An invoice should usually not.
Inventory quantities, payment records, approval states, and accounting data need stronger rules. Depending on the domain, the correct answer may be server authority, explicit conflict review, selective merge, row versioning, or compensating transactions instead of rewriting history.
The important part is that the strategy is designed intentionally.
Synchronization should be observable
When an offline system fails silently, users assume the transaction was completed.
I like to expose clear synchronization states in both UI and logs:
- Pending items count.
- Last successful sync.
- Current sync progress.
- Failed operations requiring attention.
- Retry actions.
- Diagnostic IDs for support.
For enterprise apps, observability is a product feature.
What I test before calling it offline-first
I intentionally test ugly situations:
- Create data offline and kill the app.
- Restart while still offline.
- Reconnect during a write.
- Send the same operation twice.
- Simulate a 401 during sync.
- Refresh the token and continue.
- Return a validation error from the server.
- Change the same entity from another device.
- Crash during queue processing.
- Retry after partial success.
If those cases are not understood, the application is not really offline-first yet.
Offline-first is not a networking trick. In ERP and POS systems, it is distributed systems engineering on a mobile device.