Brihat Infotech Logo
Mobile

Building Offline-First React Native Apps: Architecture and Trade-offs

Field sales reps, delivery drivers, and warehouse staff cannot wait for a 4G signal. Offline-first is not a nice-to-have for operations apps — it is the baseline. Here is the architecture that makes it work.

Brihat Team

Brihat Team

Engineering Team

14 April 202613 min read
Building Offline-First React Native Apps: Architecture and Trade-offs

When Offline-First Is Non-Negotiable

There is a category of mobile application where network connectivity cannot be assumed: field operations. A delivery driver in a basement car park. A sales rep in a rural area. A warehouse worker in a steel-framed building that blocks signal. For these users, an app that requires connectivity is an app that does not work.

Offline-first architecture inverts the assumption: the local database is the source of truth for the user interface, and the network is an optional synchronisation channel. This post covers the architecture, the sync patterns, and the conflict resolution strategies we use in production operations apps.

The Local Database Layer

In React Native, the two practical choices for a local database are WatermelonDB and SQLite via react-native-sqlite-storage or the newer op-sqlite. For complex relational data, we use WatermelonDB — it is built specifically for React Native, uses lazy loading by default, and has synchronisation as a first-class concept.

For simpler use cases — an app that needs to queue form submissions or store a few hundred records — AsyncStorage with a thin serialisation layer is sufficient. Do not reach for a full relational database if you do not need one.

The Sync Architecture

WatermelonDB's sync protocol is well-designed and covers most cases. The high-level pattern:

  1. The client sends a lastSyncedAt timestamp to the server.
  2. The server returns two arrays: changes (records created or updated since that timestamp) and deleted (IDs of records deleted since that timestamp).
  3. The client applies the server changes to the local database.
  4. The client sends its own local changes (created/updated/deleted since the last sync) to the server.
  5. The server applies the client changes and returns the new sync timestamp.

The server-side implementation requires that every row in every synchronised table has an updated_at timestamp that is updated on every write — enforced at the database level with a trigger, not in application code. It also requires a deleted_at soft-delete column, because hard-deleting a row means you cannot tell clients about the deletion.

Conflict Resolution

When the same record is modified on the client and on the server between syncs, you have a conflict. There is no universally correct resolution strategy — the right approach depends on what the data represents.

Last-write-wins: The most recent write (by timestamp) takes precedence. Simple to implement, appropriate for data where the latest state is always the correct state (GPS coordinates, status updates).

Server-wins: The server always wins. Appropriate for data that multiple users can edit, where the server version represents a consensus state (inventory counts, booking slots).

Client-wins: The client always wins. Appropriate for data that the user owns and should be able to edit locally without interference (personal notes, draft forms).

Field-level merging: Different fields in the same record can use different strategies. A delivery record might use server-wins for assignedAt (set by dispatch) and client-wins for completedAt and deliveryPhoto (set by the driver).

Optimistic UI Updates

In an offline-first app, every user action updates the local database immediately — before the network sync. The UI reflects the local state. This is what makes the app feel fast even on slow connections: the user's action has an immediate visible effect, and the sync happens in the background.

The failure case: the background sync fails, and the server rejects the client's change. This requires a reconciliation step where the local database is rolled back to the server state. Your UI needs to handle this gracefully — either by showing an error state with a retry option, or by silently reconciling if the rejection is due to a stale read.

Binary Data: Photos and File Uploads

Operations apps frequently require photo uploads — proof of delivery, site inspection photos, document scans. The pattern: capture the photo, store it locally in the file system, queue the upload, and store only the local file path in the local database. The sync layer uploads the file to S3 when connectivity is available, then updates the database record with the S3 URL. The UI shows the local file until the upload completes.

The implementation detail that matters: queue the uploads with retry logic and exponential backoff. A failed upload at low connectivity is not an error — it is expected. The upload queue should survive app restarts.

Background Sync

On iOS and Android, background processing is heavily restricted. For foreground apps, you can trigger sync on app resume, on network state change (using NetInfo), and on a timer. For background sync, you are limited to Background App Refresh on iOS and Background Task on Android — both of which have severe constraints on execution time and frequency.

The practical approach: sync aggressively when the app is in the foreground, and treat background sync as a best-effort supplement. If your operations workflow requires real-time background updates, push notifications triggering a foreground sync are more reliable than background tasks.

Building something?

Let's talk. Free 30-min scoping call with no commitment.

Let's Talk →
Brihat Team

Brihat Team

Engineering Team

The Brihat Infotech engineering team builds enterprise-grade digital systems — platforms, SaaS products, AI integrations, and workflow automations for clients across healthcare, fintech, edtech, and logistics.

Back to Blog
Found this useful? Share it.

Enjoyed this article?

Get more like it in your inbox. Practical engineering thinking from the Brihat team — once or twice a month. No spam, ever.