What Is Offline-First?
Offline-first is a design approach where the app is built to work without an internet connection as the default state. Network connectivity is treated as an enhancement, not a requirement. Data is stored locally first, and synchronization with the server happens when a connection is available.
This is different from simply caching data for offline access. In an offline-first app, users can create, read, update, and delete data while offline, and those changes are synced reliably when the device comes back online.
Why Build Offline-First?
- Reliability: Mobile connections are unreliable. Elevators, subways, rural areas, and airplane mode are everyday realities.
- Performance: Reading from a local database is orders of magnitude faster than a network request.
- User experience: No loading spinners, no "no internet" error screens, no lost work.
- Global reach: Many users in emerging markets have intermittent or expensive data connections.
Architecture Overview
An offline-first architecture has three layers:
1. Local Database (Source of Truth)
The app reads and writes to a local database. The UI is always populated from local data, never directly from API responses.
Options by platform:
- iOS native: Core Data, SwiftData, or SQLite (via GRDB)
- Android native: Room (SQLite wrapper)
- React Native: WatermelonDB, SQLite (expo-sqlite), or MMKV (key-value)
- Flutter: Drift (SQLite), Hive (NoSQL), or Isar
2. Sync Engine
A component that manages bidirectional data synchronization between the local database and the remote server. It tracks changes, sends local mutations to the server, pulls server changes, and handles conflict resolution.
3. Remote API
Your backend API that serves as the canonical long-term data store.
Sync Strategies
Timestamp-Based Sync
Each record has an updatedAt timestamp. During sync, the client asks for everything changed since the last sync, merges server changes, and sends local changes. Simple to implement but has edge cases with clock skew.
Change Tracking (Operational)
Track individual operations (create, update, delete) in a local queue. During sync, the queue is replayed against the server. Successfully synced entries are removed from the queue. More reliable than timestamp-based sync.
CRDTs (Conflict-Free Replicated Data Types)
Mathematical structures that guarantee eventual consistency without conflict resolution. Counters, sets, and maps that can be merged from any direction. Libraries like Yjs and Automerge bring CRDTs to application development.
Conflict Resolution
When the same record is modified both locally and on the server, you have a conflict. Common strategies:
- Last write wins: The most recent timestamp wins. Simple but can lose data.
- Server wins: Server version always takes priority.
- Client wins: Local version takes priority.
- Merge fields: Compare individual fields and keep the most recent value per field.
- User decides: Present both versions and let the user choose.
Practical Patterns
Optimistic UI
Apply changes to the local database and update the UI immediately. Sync in the background. The user sees instant responses without waiting for the network.
Queue and Retry
Maintain a persistent queue of pending changes. When connectivity returns, process the queue in order. Implement exponential backoff for failed requests.
Tools and Libraries
| Tool | Platform | Type |
|---|---|---|
| WatermelonDB | React Native | SQLite with sync primitives |
| PowerSync | Cross-platform | Postgres-backed real-time sync |
| Realm (Atlas Device Sync) | Cross-platform | Document DB with built-in sync |
| Firebase/Firestore | Cross-platform | Cloud-first with offline caching |
| SQLite + custom sync | Any | Full control, more work |
Best Practices
- Design the data model for sync from the start. Retrofitting offline support is painful.
- Use UUIDs for primary keys (auto-increment IDs cause conflicts across devices)
- Test offline scenarios early and often
- Show sync status to the user (synced, pending, conflict)
- Handle the "first launch with no data" state gracefully