Offline-first apps sound like the future: instant loading, privacy by default, and no more spinning loaders on flaky connections. But in practice, very few apps get offline support right. Most simply queue changes locally and push them when the network comes back (spoiler: this doesn’t really work). Eventually, users see a scary banner saying “changes may not be saved.” The reason is simple: syncing is hard. When you build a local-first app, you’ve effectively created a distributed system. Multiple devices can mutate data independently, sometimes offline, and later must converge to the exact same state without losing data. There are two main challenges to solve: Unreliable ordering Conflicts Let’s go through them. 1. Unreliable Ordering In a distributed environment, events happen on multiple devices at different times. If you just apply them as they arrive, you get inconsistent results. Example: Device A sets x = 3 Device B sets x = 5 Both happen while offline. When the devices sync, the final value of x depends on which update is applied first. That’s a problem. Traditional backend databases solve this with strong consistency, but that requires global coordination—too slow and brittle for local-first systems. Instead, we want eventual consistency: every device applies changes independently but eventually converges to the same result once all changes are known. The challenge is figuring out the correct order of events without relying on a centralized clock (because the network might be down). The Solution: Hybrid Logical Clocks (HLCs) Surprisingly, there’s a simple solution to this seemingly hard problem: Hybrid Logical Clocks (HLCs). HLCs generate timestamps that are: Comparable (can be sorted lexicographically) Causally consistent (encode the order in which events happened) HLCs combine two pieces of information: Physical time (from the machine clock) Logical time (a counter that increments when clocks are out of sync or events happen too close together) In plain terms, HLCs let devices agree on “what happened first” without perfectly synchronized clocks. A Quick Example Imagine two machines, A and B: Machine A records an event at 10:00:00.100 . → Its HLC becomes (10:00:00.100, 0) (time + counter). A sends a message to B with this HLC. Machine B’s clock shows 10:00:00.095 (slightly behind). When B receives the message, it advances its HLC to at least match A’s timestamp. B’s HLC becomes (10:00:00.100, 1) — the counter increments to indicate this happened after A’s event. Result: Event on A: (10:00:00.100, 0) Event on B: (10:00:00.100, 1) Even though B’s physical clock was behind, we can now order events consistently across machines. 2. Conflicts Even with proper ordering, conflicts still happen when two devices modify the same data independently. Example: Initial balance = 100 Device A: +20 → balance = 120 Device B: -20 → balance = 80 When they sync, which value should “win”? If you naively apply both updates, one overwrites the other—losing user data. Most systems ask developers to write manual conflict resolution code, but that’s error-prone and hard to maintain. The Solution: CRDTs The right approach is CRDTs (Conflict-Free Replicated Data Types). CRDTs guarantee two important properties: Commutativity: Order of application doesn’t matter Idempotence: Applying the same change twice has no effect This means you can apply messages in any order, even multiple times, and every device will still converge to the same state. One of the simplest CRDT strategies is Last-Write-Wins (LWW): Each update gets a timestamp (physical or logical). When two devices write to the same field, the update with the latest timestamp wins. Example: Device A: balance = 120 at 10:00:00 Device B: balance = 80 at 10:00:02 When syncing, the system keeps 80 because it was written last. Why SQLite Is Perfect for This When building a local-first app, you need a rock-solid local database. SQLite is the obvious choice: battle-tested, lightweight, and available everywhere. That’s why we built our local-first framework as a SQLite extension. Our approach (simplified): Every change is stored as a message in a messages table with: timestamp (from HLC) dataset (table name) row (encoded primary keys) column value Applying a message is as simple as: Look up the current value If the incoming timestamp is newer → overwrite If it’s older → ignore This guarantees convergence across devices, regardless of the sync order. Why This Matters This architecture makes syncing simple and reliable: Reliable: Survives weeks of offline use without data loss Deterministic: Final state always converges Minimal: Just a small SQLite extension, no heavy dependencies Cross-platform: The extension is available for iOS, Android, macOS, Windows, Linux, and WASM Takeaways for Developers Stop faking offline support with request queues Embrace eventual consistency Use proven distributed-systems techniques like HLCs and CRDTs Keep it small and dependency-free The result? Apps that are instant, offline-capable, and private by default — without the complexity of traditional client–server synchronization. If you need a production-ready, cross-platform, offline-first engine for SQLite, check out our open-source SQLite-Sync extension.