Connectivity in Accra can drop mid-transaction. In Tamale, a field agent may work for six hours without a signal bar. In Lagos, a user might switch between 4G and EDGE three times in a single minute. If your mobile app treats the network as a given, you have already failed the majority of your users before they open it.
Offline-first is not a feature. It is an architectural philosophy — and in African markets, it is table stakes.
What "Offline-First" Actually Means
Offline-first does not mean "works when there is no internet." That is a side effect. It means the app's primary data layer lives on the device, and the network is used opportunistically to synchronise state rather than to deliver it.
The mental model shift is this: instead of fetching data from a server and caching it locally as a fallback, you write data locally first and push it to the server when conditions allow. The local store is the source of truth. The server is the eventual destination.
This distinction matters enormously for UX. Users should never see a loading spinner for data they have already interacted with. Every read should be instantaneous. Every write should be acknowledged immediately, even if sync is pending.
Choosing Your Local Data Layer
For React Native applications, the embedded database landscape has matured considerably. Three options stand out:
- WatermelonDB — Built specifically for React Native, it is lazy-evaluated, uses SQLite under the hood, and is designed from the ground up for sync. It handles thousands of records without performance degradation. This is the go-to for complex, relational offline data.
- MMKV — A blazing-fast key-value store backed by Tencent's memory-mapped file format. Ideal for lightweight state, user preferences, and session tokens that need persistence across restarts.
- SQLite via expo-sqlite or op-sqlite — Raw SQL control when you need custom query patterns or are migrating an existing schema.
For most production offline-first apps built at Code!nk, WatermelonDB combined with a custom sync adapter gives the best balance of query power and sync flexibility.
Architecting the Sync Layer
Sync is where offline-first architecture gets genuinely hard. The pattern that holds up best in low-bandwidth environments is a delta sync with a change log.
The idea: every mutation on the client (create, update, delete) is written to a local changes table with a timestamp and operation type. When connectivity is detected, the app ships only the delta — the set of changes since the last successful sync — rather than the full data payload.
On the server side, every record should carry a server_updated_at timestamp. The client sends its last_synced_at, and the server returns only records modified after that watermark. This bidirectional delta approach keeps payloads tiny, which matters when users are on 2G or paying per megabyte.
A minimal sync cycle looks like this:
async function syncWithServer(lastSyncedAt: number) {
const localChanges = await db.collections
.get('changes')
.query(Q.where('synced', false))
.fetch();
const { serverChanges, newSyncTimestamp } = await api.sync({
changes: localChanges.map(serializeChange),
lastSyncedAt,
});
await db.write(async () => {
await applyServerChanges(serverChanges);
await markChangesSynced(localChanges);
await updateSyncMetadata(newSyncTimestamp);
});
}
Wrap this in an exponential backoff strategy and trigger it on NetInfo connectivity events, not on a naive timer. Polling wastes battery and data — event-driven sync respects both.
Conflict Resolution Without Drama
Two users editing the same record offline is not a corner case in field operations — it is Tuesday. You need a conflict resolution strategy defined before you write a single line of sync code.
Three approaches, in order of increasing complexity:
Last-write-wins (LWW): The record with the latest timestamp survives. Simple, predictable, and sufficient for most app types — user profiles, inventory counts, form submissions where only one actor is expected per record.
Merge by field: Instead of resolving at the record level, resolve at the field level. If User A updated quantity and User B updated notes, both changes survive. This requires your schema to track field-level timestamps or version vectors.
Manual resolution queue: For high-stakes data — financial records, patient data, legal documents — surface conflicts to a privileged user for manual review. Store both versions, flag the record, and present a diff UI. This is more engineering work but the only responsible approach for data where automated merges carry real risk.
Document your conflict strategy per entity type. A health worker's patient intake form and a retail app's product catalogue have very different tolerance for data loss.
Bandwidth-Aware Media Handling
Images and files are the biggest sync liability on low-bandwidth networks. Do not treat them the same as structured data.
Use a deferred upload queue for any binary assets. When a user attaches a photo to a record, store it locally immediately, reference it by a local URI, and queue the upload separately. The record syncs first (fast, small payload). The image follows when bandwidth permits, and the server links them once both arrive.
Compress aggressively on-device before upload. A field photo captured at full camera resolution is rarely necessary. Resize to a maximum dimension and compress to JPEG at 70–80% quality before it ever touches the upload queue. On a 3G connection, this can be the difference between a three-second upload and a thirty-second timeout.
Testing for the Real Network
Development on a fibre connection is dishonest. Build a network condition simulator into your QA process. Both Android emulators and iOS simulators support network throttling — use the "Edge" and "3G" profiles routinely, not just before launch.
More importantly, test network interruption mid-sync. Kill the connection halfway through a delta push. What happens to the changes table? Does the next sync retry cleanly or does it double-write? These are the bugs that reach production and erode user trust the most.
Why This Matters for Your Project
If you are building a mobile product targeting users across West Africa, East Africa, or any market where connectivity is a variable rather than a constant, offline-first architecture is not optional engineering overhead — it is the competitive moat. Apps that work reliably in the field earn loyalty that cloud-dependent competitors cannot touch. The engineering investment in a solid local data layer and a well-designed sync protocol pays back in retention, in referrals, and in the ability to serve users that other products have written off. Build for the network you have, not the one you wish you had.




