Offline-First in Flutter: The Sync Strategy I Actually Use
How I handle data conflicts and sync in the Ventri Farm app — which must keep working in barns without signal.
Wafik Ulinnuha
Backend Developer
The Ventri Farm app runs in chicken barns, far from cell towers. Signal drops every few minutes. A UX that shows a loader on disconnect is the same as unusable. The solution is offline-first design.
The principles I hold to
- Write locally first. Every user action becomes a record in SQLite with
local_id,created_at, and async_statusflag. - The server is the ultimate source of truth. The local store is an authoritative cache only until sync succeeds.
- Conflicts resolve deterministically. No "choose a version" dialog for field operators.
The local schema
I use drift (formerly Moor) in Flutter. Every domain table has a shadow table for the sync queue:
class FieldEntries extends Table {
TextColumn get localId => text()();
TextColumn get serverId => text().nullable()();
IntColumn get version => integer().withDefault(const Constant(0))();
TextColumn get payload => text()(); // JSON
IntColumn get syncStatus => intEnum<SyncStatus>()();
DateTimeColumn get updatedAt => dateTime()();
@override
Set<Column> get primaryKey => {localId};
}
Conflict resolution strategy
For Ventri, most entities are append-only (event records). True conflicts are rare. For mutable entities (chicken master data), I use Last-Write-Wins backed by a simple vector clock: the server stores a version that bumps on every update, and clients must send If-Match with the last known version. The server rejects updates if its version is newer, and the client is asked to re-merge.
Background sync
I do not force sync on the main isolate. A separate worker runs every 30 seconds (foreground) or fires via workmanager every 15 minutes (background). The queue is FIFO but I batch up to 50 records per request to keep payloads sane the moment 4G returns.
What I would do differently
- Adopt CRDTs (like Automerge) from day one for collaborative entities. For single-operator-per-record, plain LWW is fine.
- Be more aggressive about sync metrics: how long do records sit in the queue? What is the conflict ratio? Without numbers, optimization is guesswork.
Offline-first is not an add-on feature. It changes how you think about your domain model. The investment is real — but so is the return: an app that does not blame the operator when the WiFi disappears.
Further reading
Further reading
Designing Multi-Tenant SaaS on PostgreSQL Without Regret
Your multi-tenant strategy locks in operational cost, security posture, and migration pain for years. Here is an honest guide from building Gatsu.
Astro Islands vs Next.js App Router: Choosing With Context
It is not about which is faster. It is about which fits the shape of the product and the team. Here are the questions I ask before deciding.