Every field app we've shipped in East Africa shares one constraint: connectivity is a luxury, not a guarantee. MyAfya Profile, Uboraschool, and Anza Connect all needed users to keep working when the network disappeared — and sync cleanly when it came back.
Here's the architecture we reach for when "offline-first" is a requirement, not a nice-to-have.
1. Local source of truth
The device owns the data while offline. SQLite (via Drift on Flutter, or SQLDelight on Kotlin) stores every mutation in an append-only queue. Reads never wait on the network.
// Every write goes local first
await db.insert('pending_ops', {
entity: 'booking',
payload: jsonEncode(booking),
created_at: DateTime.now().toIso8601String(),
});
2. Conflict-free reconciliation
We avoid last-write-wins for business-critical entities. Bookings, payments, and inventory use server-assigned version vectors or domain-specific merge rules — documented before a single screen is built.
If you can't explain what happens when two guides edit the same manifest offline, you're not ready to build offline-first.
3. Sync as a state machine
Sync is never a single API call. It's a pipeline: push pending ops → receive server deltas → reconcile → mark clean. Each stage is retryable, logged, and visible in the UI.
- Exponential backoff with a ceiling (max 5 min between retries)
- User-visible sync status — never silent failure
- Idempotent server endpoints keyed by client operation ID
What we ship with every offline app
Tests that simulate airplane mode. A staging environment with artificial latency. Handover docs that explain the sync contract in plain language. Your team should own this — not wonder how it works six months later.
Building something that needs to work off the grid? Tell us what you're shipping — we've done this before.