~/mesha-vegas$⌘
$ cd ../

2026-07-24

Offline-first: coding for a connection that doesn't exist

In Africa, connectivity is neither continuous nor free. Here's how I design applications that work without a network, and why offline mode cannot be added at the end.

  • #offline-first
  • #react-native
  • #expo
  • #afrique
  • #synchronisation
  • #architecture
$7 min read28 reads
Offline-first: coding for a connection that doesn't exist

There's a sentence I hear in nearly every project kickoff: "we'll handle offline mode later." Same energy as "we'll add the tests after V1." It never happens, and when it does, everything has to be rewritten.

On Driivio, a highway-code learning platform for the Cameroonian market, I made the opposite choice: the app was designed offline first, online second. That wasn't an aesthetic decision. It was the only way to have a usable product.

The real problem isn't the absence of network

When a European developer thinks "offline", they think of the metro, a tunnel, a plane. A clean, short cut, and the network comes back.

That's not my users' reality.

A driving-licence candidate in Bafoussam has network. They even have 4G. What they don't have is unlimited data. They buy a bundle, they consume it, and by day three they're browsing on 2G or not at all until the next top-up. In between lies a grey zone far more painful than a clean cut: the connection that answers in twelve seconds, the one that opens the socket then gives up, the one that downloads 80% of a response before dying.

Airplane mode is the easy case: navigator.onLine is false, you show a banner, done. The dying connection lies. navigator.onLine is true. Your request leaves. It doesn't come back. And your loading spins into the void while the user closes the app.

Hunter x Hunter gives the best image of this. The most powerful Nen in the manga isn't the one with the most raw power: it's the one that imposes conditions and restrictions. Kurapika becomes able to kill a member of the Phantom Troupe precisely because he accepts death if he uses his chains against anyone else. The constraint creates the power.

Designing offline-first means imposing a restriction: my app must be fully usable with no request ever succeeding. That constraint, held from day one, produces a markedly more solid architecture than adding it afterwards.

What it changes concretely

The local database is the source of truth

The usual reflex: I query the server, display the result, cache it just in case. The cache is an optimisation.

Offline-first inverts this. The app always reads the local database. The network only feeds that database, in the background, when it can. The interface doesn't even know whether you're connected — it reads what it has.

That has a pleasant consequence: your app becomes instant. No spinner on the main screens, ever. The content is already there. Users with a good connection benefit as much as everyone else.

Writes are intentions, not requests

When a candidate finishes a question set, the app doesn't send a POST. It writes an intention to a local queue: "set 47 completed, 18/20, at 14:32."

The interface confirms immediately. The queue drains when the network allows, in order, with spaced retries.

Three rules make this viable:

  • Each intention carries a client-generated identifier. Without it, a response lost after server processing produces a duplicate on retry.
  • The server is idempotent. Receiving the same intention twice must produce the same final state. That's the backend's guarantee, not the client "being careful".
  • The timestamp comes from the client, but the server's is kept. Both serve: one for the order the user perceives, the other to arbitrate conflicts.

Conflicts exist; you have to choose your rule

Two devices, one account, two offline edits. What do you keep?

There's no universally right answer, only a decision to own per data type:

  • Learning progress: merge, keep the best score. Nobody complains about getting their best mark back.
  • User profile: last write wins. The risk is low, the cost of a merge high.
  • Payment: never offline. A payment happens online or it doesn't happen. There is no local version of a Mobile Money debit.

That last line is the one developers forget most often. Not everything deserves to be available offline. Deciding what is not is part of the job.

The initial-download trap

An offline-first app has to fill its local database. When? How?

The naive solution — download everything on first launch — is the worst. Your user installs the app, it demands 80 MB, they're on a metered bundle, they uninstall. You lost them before the first screen.

What works:

  1. A minimal core bundled in the binary. The first question sets ship inside the APK. The user can work a second later.
  2. The rest in batches, on demand. They download the next module when they need it, knowing the stated size.
  3. An indicator of what's available offline. A simple badge on each module. The user becomes an actor in their own data consumption.

That third point went from "interface detail" to requested feature. People want to know what they're about to spend. Respecting the user's data bundle is just respect.

The middle problem: nobody tests this

Here's where the profession falls down.

Your dev environment has fibre. Your emulator has fibre. Your CI has fibre. Your product manager tests from an office with fibre. The entire production chain lies about real usage conditions.

Tools don't save you: devtools' "Slow 3G" simulates steady slowness. Reality is irregularity — 4G for eight seconds, nothing for twenty, then a spike. Uniform slowness is easy to handle; it's unpredictability that breaks applications.

What I do, for lack of better:

  • A chaos mode in dev preferences: random loss of X% of requests, latency between 200 ms and 30 s. On by default in development.
  • A real entry-level phone, not a flagship, with a nearly exhausted bundle. One hour per sprint. It clears the mind.
  • The hot airplane-mode test: cut the network mid-journey, not before starting it. That's where the bugs are.

The honest cost

I won't sell this as free. Offline-first is expensive:

  • 30 to 40% more development on the affected features.
  • Real complexity: queue, conflict resolution, local schema migrations.
  • Harder debugging: state is split between client and server.

In exchange you get an app that works in your users' real world. In a market where competitors ship interfaces that say "Check your connection" three times per session, that's an advantage visible in use, not on a feature sheet.

And there's an underrated side benefit: offline-first apps are faster for everyone. Your user in Paris on fibre also enjoys an interface that never blocks on a network call.

Where to start, from an existing codebase

You don't rewrite everything. You proceed in order of usefulness:

  1. Identify the main journey. The one 80% of users take. Nothing else for now.
  2. Make it readable offline. Read-only, local database, no write sync. That's already 60% of the perceived value.
  3. Add the write queue on a single action, the most frequent one.
  4. Instrument. How many requests actually fail for your users? The number always surprises.

That last point is the real trigger. Until you've measured your users' real network failure rate, you're debating opinions. Once you see that 12% of requests don't complete, the discussion is over.


This design approach is what I apply on Driivio, and the same principles run through Pla'nif. If you're building a product for a market where connectivity is paid by the megabyte, write to me — the first conversation is free.