← Flutter Reference

Offline-First vs Local-First

Architecture

By Abdelrahman Saed — Senior Mobile Engineer. Last updated: 2026-08-10.

Quick answer

Offline-first means the app is designed to tolerate disconnection — it caches, queues writes, and degrades gracefully — but the cloud is still the source of truth. Local-first means the on-device database is the source of truth and sync is a background reconciliation, not a primary read/write path. Local-first is the stronger guarantee and the harder architecture.

At iStoria we run a local-first model: PowerSync keeps each device's local Drift/SQLite database authoritative, and the UI never blocks on network. The distinction is not academic — it determines whether the app is usable on a flaky train, and whether concurrent edits survive.

Feature comparison

FeatureOffline-FirstLocal-First
Source of truthCloud / serverOn-device database
ReadsNetwork → cache fallbackLocal SQLite (instant)
WritesTo server (queued if offline)To local (synced later)
Offline writesQueued, replayedFirst-class, immediate
Conflict resolutionUsually LWW or manualBuilt into sync layer
Network dependency for UXModerateMinimal
Architecture complexityLower to startHigher upfront
Sync engineOften hand-rolledPowerSync / CRDT / ElectricSQL
Best forRead-heavy, server-authoritativeCollaborative, write-heavy, field apps

Detailed comparison

The source-of-truth question

This is the core difference and it propagates everywhere.

Offline-first: the server database is authoritative. The app reads from network, caches locally for speed/offline reads, and writes go to the server when online (or are queued and replayed). On conflict, the server usually wins (last-write-wins or app-specific merge).

Local-first: the local database is authoritative. Every read and write hits local SQLite instantly. A sync engine (PowerSync, ElectricSQL, CRDT libraries) reconciles changes in the background. The app is fully functional with zero connectivity, and conflict resolution is part of the sync layer, not bolted on.

Implications for the data layer

Offline-first typically means: a repository that tries network first, falls back to cache for reads, and a write queue (often hand-rolled) for offline mutations. State management must handle "optimistic vs confirmed" states.

Local-first means: the repository reads/writes local SQLite unconditionally. A separate sync subsystem watches for connectivity and runs reconciliation. The repository does not know or care about the network — that is the sync engine's job. This is a cleaner separation and is exactly what PowerSync provides.

Conflict resolution

Offline-first apps usually punt: last-write-wins on the server, or a manual merge screen for known conflict cases. This breaks under real concurrent edits.

Local-first architectures bake conflict resolution into the sync layer. PowerSync applies server-side deterministic resolution; CRDT-based libraries (Yjs, Automerge) do field-level merges. The architecture forces you to think about merges upfront, which is the only honest way to handle distributed writes.

Complexity and cost

Local-first is more architectural investment: sync rules, conflict policy, schema that supports syncing (timestamps, soft deletes, client IDs). Offline-first is easier to start with but accumulates merge debt as the app grows.

Which to choose

Choose local-first when the app must be fully usable offline with writes (field apps, note-taking, collaborative tools, content creation). Choose offline-first when offline is a read-only degradation (news apps, dashboards) or when writes are rare and server-authoritative.

Code comparison

Offline-first repository — network-first, cache fallback

class ArticleRepo {
  final Dio dio;
  final Cache cache;

  Future<Article> fetch(String id) async {
    try {
      final r = await dio.get('/articles/$id');
      final a = Article.fromJson(r.data);
      await cache.put(id, a); // cache for offline reads
      return a;
    } on DioException {
      return cache.get(id); // degrade to cache
    }
  }

  // write — queued if offline (you build the queue)
  Future<void> bookmark(String id) async {
    await writeQueue.enqueue(() => dio.post('/bookmarks', data: {'id': id}));
  }
}

Local-first repository — local SQLite is authoritative, PowerSync syncs

class ArticleRepo {
  final PowerSyncDatabase db;

  Future<Article> fetch(String id) async {
    // read local — always instant, always works offline
    final rows = await db.execute('SELECT * FROM articles WHERE id = ?', [id]);
    return Article.fromRow(rows.first);
  }

  // write local — immediate, PowerSync reconciles upstream
  Future<void> bookmark(String id) async {
    await db.execute('INSERT INTO bookmarks (id) VALUES (?)', [id]);
  }
}
// sync runs in the background; the repo never touches the network

The local-first version is simpler in the repository because the sync engine owns the network. The offline-first version forces the repo to handle network-vs-cache branching and a write queue.

Which should you choose?

Choose offline-first when: offline is primarily a read-caching concern, writes are server-authoritative and infrequent, the app degrades acceptably without connectivity, or you want a simpler initial architecture. News, dashboards, and admin tools fit here.

Choose local-first when: the app must be fully functional offline including writes, you have concurrent edits that need real conflict resolution, or the UX cannot tolerate network latency on the read/write path. Note-taking, field/data-collection, and collaborative apps demand this. It is more upfront investment but the only honest architecture for write-heavy offline apps.

Related definitions

Related reading

FAQ

Is local-first just offline-first with better marketing?

No. The distinction is the source of truth. Offline-first keeps the server authoritative and tolerates disconnection; local-first makes the device authoritative and syncs as a background concern. The data-layer code is structurally different, and local-first forces real conflict resolution rather than last-write-wins.

Do I need PowerSync for local-first?

Not necessarily — you can use ElectricSQL, a CRDT library, or your own sync layer. PowerSync is a strong choice for Flutter + Postgres because it handles sync rules and conflict resolution and integrates with Drift/SQLite. The point is you need a sync engine; local-first without one is just an offline cache.

Is local-first harder to build?

Yes, upfront. You must design schema for sync (timestamps, client IDs, soft deletes), define sync rules/partitioning, and pick a conflict policy. The payoff is that the read/write path becomes simpler and the app works unconditionally offline. For write-heavy offline apps, it is the right trade.


Available for hire. Abdelrahman Saed is a Senior Mobile Engineer (Flutter) — open to full-time, fractional, contract, or advisory work. Hire me →

Book a 20-minute call · Download the CV (PDF) · See how I work