When your users work in areas with unreliable connectivity, offline-first isn't a nice-to-have — it's the whole product. Here's how we approached it with Flutter and SQLite.
In 2021, we built a housing management system for a government housing program across multiple regions in Indonesia. The challenge sounded straightforward on paper: manage rental applications, track units, and report on occupancy.
The hard part? Field officers often work in locations with no reliable mobile data. A regular online-only app would be useless to them.
Why Offline-First Changes Everything
Most apps treat offline as an edge case — a graceful degradation when the network drops. Offline-first flips this: the local device is the primary source of truth, and the server is a sync target.
This changes how you think about almost every part of the system:
- Data reads come from local storage first
- Writes go to a local queue, not directly to the server
- Conflict resolution becomes a first-class concern
- Sync is a background process, not a blocking operation
Our Tech Choices
Flutter was the natural choice for the mobile client. It gave us a single codebase for Android (the primary platform for field officers) with a clean path to iOS if needed. Flutter's widget system made it easy to build a data-dense UI that worked well on mid-range Android devices.
SQLite (via the sqflite package) handled local persistence. We modelled the local schema to closely mirror the server schema, which simplified our sync logic considerably.
Go powered the backend API. Its concurrency model handled the burst sync requests — when many devices come online after a period of offline work — without performance issues.
The Sync Strategy
We used a timestamp-based sync approach with a server-side change log:
- Every record has a
updated_attimestamp on both client and server - On sync, the client sends its last-sync timestamp
- The server returns all records changed since that timestamp
- The client merges, with server records winning on conflict (last-write-wins)
- The client pushes its local queue of pending writes
Last-write-wins is simple but has edge cases. For this project, field officers work in distinct geographic areas with very little data overlap, so true conflicts were rare enough that LWW was acceptable.
What We Learned
Test with real devices in real conditions. Emulators don't simulate the memory pressure, battery management, or network switching behaviour of actual Android devices in the field. We caught several sync bugs only during on-site testing.
Sync UI matters as much as sync logic. Users need to know whether they're looking at fresh or cached data. We added a subtle "last synced" indicator and a manual sync button — this alone reduced support questions significantly.
Keep the local schema stable. Every time you change the local SQLite schema, you need a migration for every device in the field. We were conservative about schema changes after the first deployment.
The Result
Field officers could complete their work — registering applicants, inspecting units, submitting reports — entirely offline. Data would sync automatically the next time they connected to WiFi. Processing time for housing applications dropped significantly, and the manual paperwork that had defined the previous process was eliminated.
If you're building something where connectivity can't be assumed, we'd be glad to talk through the architecture. Get in touch.