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.
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).
Surprisingly, there’s a simple solution to this seemingly hard problem: Hybrid Logical Clocks (HLCs).
HLCs generate timestamps that are:
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.
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.
Even with proper ordering, conflicts still happen when two devices modify the same data independently.
Example:
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 right approach is CRDTs (Conflict-Free Replicated Data Types).
CRDTs guarantee two important properties:
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:
When syncing, the system keeps 80 because it was written last.
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):
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.
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
-
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.

