Offline-First Mobile Apps: Sync Strategies That Work
Building offline-first apps means handling sync gracefully. Learn practical strategies to keep data consistent without frustrating users.
Your user opens your app on the subway. They add three tasks, update two contacts, and start composing a message. Then the signal drops. If your app wasn't built for this moment, you've already lost them.
Offline-first isn't a feature—it's a requirement. But getting sync right is where most teams stumble. Not because it's technically impossible, but because bad sync strategies create phantom data, conflict nightmares, and users who just uninstall.
Let's talk about sync approaches that actually work.
The Core Problem with Naive Sync
Most developers start with the obvious approach: queue changes locally, push them to the server when connection returns. Sounds simple. Then you hit conflicts.
User A edits a contact's phone number offline. User B edits the same contact's email on another device—both save. Which wins? What happens to the user's changes? How does the UI reflect this without showing corrupted data?
This is where offline-first gets real.
Strategy 1: Timestamp-Based Last-Write-Wins (LWW)
Simplest approach: attach a server timestamp to every field. When syncing, the most recent timestamp wins.
typescriptinterface SyncRecord { id: string; data: Record<string, any>; fieldTimestamps: Record<string, number>; lastSyncedAt: number; } function resolveConflict(local: SyncRecord, remote: SyncRecord): SyncRecord { const merged = { ...remote.data }; for (const [field, localValue] of Object.entries(local.data)) { const localTime = local.fieldTimestamps[field] || 0; const remoteTime = remote.fieldTimestamps[field] || 0; if (localTime > remoteTime) { merged[field] = localValue; } } return { ...remote, data: merged }; }
Trade-off: Simple to implement, but user edits can silently disappear if another device wins the timestamp race. Works well for low-conflict scenarios (notes apps, reading lists). Not great for collaborative editing.
Strategy 2: Operational Transformation (OT)
Instead of syncing final state, sync operations. If User A deletes character 5 and User B inserts at position 3, transform A's delete to account for B's insert.
Google Docs does this. So does most real-time collaborative software.
typescripttype Operation = | { type: 'insert'; position: number; content: string } | { type: 'delete'; position: number; length: number }; function transform(op: Operation, priorOp: Operation): Operation { if (op.type === 'insert' && priorOp.type === 'insert') { if (op.position <= priorOp.position) return op; return { ...op, position: op.position + priorOp.content.length }; } if (op.type === 'delete' && priorOp.type === 'insert') { if (op.position <= priorOp.position) return op; if (op.position >= priorOp.position + priorOp.content.length) { return { ...op, position: op.position - priorOp.content.length }; } // Delete spans insertion—split the delete return { type: 'delete', position: priorOp.position, length: op.length - priorOp.content.length }; } return op; }
Trade-off: Complex, stateful, requires careful testing. Essential for text editors and truly collaborative apps. Overkill for most crud operations.
Strategy 3: Sync with Conflict Windows
Accept that conflicts happen, but contain them. Give users a short window (seconds to minutes) to resolve conflicts manually, then auto-merge with clear UI feedback.
typescriptinterface ConflictAlert { recordId: string; field: string; localValue: any; remoteValue: any; timestamp: number; } function showConflictResolution(conflict: ConflictAlert) { // Show side-by-side comparison, let user choose // Log choice for audit trail // Merge and sync }
Trade-off: Requires UI investment, but prevents silent data loss. Users see exactly what happened. This is what most production apps actually do.
Practical Reality
Most offline-first apps combine these approaches. Use LWW for non-critical metadata, OT for text content, and conflict windows for everything else. At LavaPi, we've found this hybrid approach scales from small teams to thousands of concurrent users.
The key insight: sync strategy should match your data model. Don't over-engineer. Start with LWW, add complexity only when conflicts actually happen in production.
Build with offline first, not as an afterthought. Your battery life—and your users—will thank you.
LavaPi Team
Digital Engineering Company