Agriculture, climate monitoring, and supply-chain apps all share one condition: the people using them are often out of signal. A farm registration app that fails without 4G is useless where it's needed most.
The principle: local first, sync later
- The UI reads and writes only to the local database.
- Every change is recorded in an outbox.
- A sync worker sends the outbox when online and pulls server changes.
The network becomes an optimisation, not a requirement.
Local storage
| Option | Good for |
|---|---|
| expo-sqlite | Default choice: relational, reliable |
| WatermelonDB | Large datasets, reactive queries, sync primitives |
| MMKV / AsyncStorage | Small key-value settings, not records |
The outbox
type OutboxItem = { id: string; // client-generated UUID entity: "plot" | "harvest" | "photo"; operation: "create" | "update"; payload: unknown; createdAt: number; attempts: number; };
Sync loop:
- Take the oldest items.
- Send them with their client-generated IDs.
- On success, mark them done. On failure, back off and retry.
Idempotency matters. The server must accept the same item twice without creating duplicates, because retries will happen. Client-generated UUIDs make this easy.
Photos and large files
Save to the device file system, queue uploads separately, and upload with resumable or chunked uploads when on Wi-Fi if possible. Never block a form on a photo upload.
Conflicts
| Strategy | When |
|---|---|
| Last write wins | Simple fields, low risk |
| Append-only events | Harvests, measurements, logs (no conflicts by design) |
| Field-level merge | Different people edit different fields |
| Manual review | Critical data like plot boundaries |
For traceability data, I prefer events over mutable records. See Agricultural Supply-Chain Traceability.
UX that builds trust
- Show sync status per record: saved on device, syncing, synced, or failed.
- Show last successful sync time.
- Never say "Saved" when you mean "queued".
- Let users retry failed items.
- Work on cheap Android devices with limited storage.