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.