Offline-First Web & Mobile Architecture: CRDTs, Local SQLite & Real-Time Sync Protocols
Architectural blueprint for building zero-latency, resilient offline-first applications. Discover how Conflict-Free Replicated Data Types (CRDTs), client-side SQLite, and bidirectional delta synchronization guarantee data integrity.
1. The Reality of The Modern Edge: Offline is Not an Edge Case
In modern software engineering, treating network connectivity as a guaranteed precondition is a fatal architectural flaw. Mobile users travel through subways and rural zones; retail store POS cash registers lose Wi-Fi during peak sales; warehouse inventory barcode scanners operate inside signal-shielded metal facilities.
Traditional web applications freeze with blocking spinners the instant the network disconnects. A transaction initiated offline fails silently or throws unrecoverable errors.
In contrast, an 'Offline-First' architecture treats local client storage (SQLite via WASM in web browsers, native SQLite on mobile) as the single source of truth for the UI. Writes execute locally with 0ms latency, and background synchronization handles eventual consistency whenever connectivity returns.
2. Conflict Resolution: Last-Write-Wins vs CRDTs
The central engineering hurdle of offline-first systems is conflict resolution: what happens when Device A edits an invoice offline at 10:02 AM, and Device B edits the same invoice offline at 10:04 AM?
Two primary resolution methodologies exist:
1. Last-Write-Wins (LWW): Relies on timestamps. Whichever update has the later timestamp overwrites the previous update. While simple to implement, LWW suffers from clock drift and causes silent data loss when concurrent edits touch different fields.
2. Conflict-Free Replicated Data Types (CRDTs): Mathematical data structures (such as Yjs or Automerge) that can be edited independently and concurrently across distributed peers. CRDTs guarantee mathematical convergence: regardless of the order or delay in which delta packets arrive, all clients converge on the identical state without human intervention or data loss.
3. Implementation: Building a Resilient Offline Sync Queue in TypeScript
Below is an enterprise-grade sync queue implementation that writes mutations to IndexedDB/SQLite, assigns monotonic logical version numbers, and drains the queue with retry backoff when online:
export interface MutationTask {
id: string;
entity: string;
operation: "INSERT" | "UPDATE" | "DELETE";
payload: Record<string, unknown>;
createdAt: number;
retryCount: number;
}
export class OfflineSyncManager {
private queue: MutationTask[] = [];
private isSyncing = false;
constructor() {
// Listen to native browser connectivity events
window.addEventListener("online", () => this.flushQueue());
}
async enqueueMutation(task: Omit<MutationTask, "id" | "createdAt" | "retryCount">): Promise<void> {
const fullTask: MutationTask = {
...task,
id: crypto.randomUUID(),
createdAt: Date.now(),
retryCount: 0,
};
// 1. Persist locally to client IndexedDB/SQLite
await this.persistLocalTask(fullTask);
this.queue.push(fullTask);
// 2. Attempt background sync if connected
if (navigator.onLine) {
this.flushQueue();
}
}
async flushQueue(): Promise<void> {
if (this.isSyncing || this.queue.length === 0 || !navigator.onLine) return;
this.isSyncing = true;
try {
while (this.queue.length > 0) {
const current = this.queue[0];
const success = await this.pushToServer(current);
if (success) {
this.queue.shift(); // Remove from memory queue
await this.removeLocalTask(current.id);
} else {
current.retryCount++;
if (current.retryCount > 5) {
// Escalate to dead-letter storage
await this.markTaskFailed(current);
this.queue.shift();
} else {
break; // Pause and wait for next interval
}
}
}
} finally {
this.isSyncing = false;
}
}
private async pushToServer(task: MutationTask): Promise<boolean> {
try {
const res = await fetch("/api/sync/mutations", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(task),
});
return res.ok;
} catch {
return false;
}
}
private async persistLocalTask(task: MutationTask): Promise<void> { /* Storage save */ }
private async removeLocalTask(id: string): Promise<void> { /* Storage delete */ }
private async markTaskFailed(task: MutationTask): Promise<void> { /* Dead-letter */ }
}4. Case Study: Deploying Offline POS across 60 Gulf Retail Stores
CodeXox engineered an offline-first Point of Sale (POS) rollout for a regional retail group operating 60 physical branch stores across Saudi Arabia. The stores frequently experienced intermittent fiber dropouts.
Our architecture embedded a lightweight SQLite engine in each register terminal. Barcode scanning, receipt calculation, and cash/card ledger entries executed locally in under 4ms. Stock deductions were serialized as CRDT delta operations.
When an internet outage lasted 4 hours during a peak weekend sale, cashiers continued ringing customers with zero downtime. Upon network recovery, over 14,000 offline transactions reconciled automatically in under 90 seconds without a single ledger discrepancy.
- Zero customer delays during upstream telecom outages.
- Instant receipt printing with local ESC/POS thermal printer commands.
- Automatic stock count reconciliation using vector clocks.
Can SQLite run directly in web browsers?
Yes! Modern web browsers run official SQLite compiled to WebAssembly (WASM) with Persistent Origin Private File System (OPFS) storage, achieving near-native disk I/O performance.
How do you handle authentication in offline apps?
Use asymmetric cryptography and offline session grants. Store a cryptographic public key locally; authenticated offline shifts can sign local receipt manifests that are verified upstream during reconciliation.
What is the bandwidth footprint of CRDT sync?
Modern CRDT libraries like Yjs pack updates into highly compressed binary buffers, typically requiring less than 80 bytes per typical document state mutation.