Writing

Local-first, and why I keep coming back to it

Sync is hard, but building as if the network is a feature rather than a dependency changes the whole shape of an app — usually for the better.

Most apps treat the network as load-bearing. The server is the source of truth; the client is a thin window onto it. Lose the connection and the window goes dark. We’ve normalized this so thoroughly that a spinner over a grey rectangle reads as correct — as just how software behaves.

Local-first flips the relationship. The source of truth lives on your device. The network exists to sync that truth between your devices and your collaborators — but the app works, fully, before any of that happens. The network becomes a feature that makes a working app better, not a dependency that a working app can’t exist without.

What changes when you build this way

The obvious win is offline. You can use the app on a plane, in a tunnel, on hotel wifi that’s technically connected but functionally useless. But offline is the least interesting benefit, and selling local-first on offline alone undersells it badly.

The first non-obvious win is speed. When reads and writes hit local storage first, every interaction is instant — there’s no round trip to wait on, so there’s no spinner to design around. You stop building loading states because there’s nothing to load. The fastest network request is the one you didn’t make, and local-first turns most requests into ones you don’t make.

The second non-obvious win is resilience. Your app stops having a single point of failure that takes the whole experience down. The server being slow, or down, or rate-limiting you becomes a background-sync problem — something that resolves itself quietly later — rather than a the-user-is-staring-at-a-blank-screen problem. Your worst day degrades to “sync is a little behind” instead of “the product is unusable.”

The third, and the one I didn’t expect, is that it makes you honest about your data model. When the client owns the truth, you have to think clearly about what a record is, who can change it, and what happens when two people change it at once. That clarity tends to produce better systems even in the parts that never go offline.

The hard part, honestly

Sync is genuinely difficult, and I won’t pretend otherwise. The moment two devices can edit the same data while offline, you’re in conflict-resolution territory, and “last write wins” quietly eats people’s work. Someone edits a note on their phone, edits it differently on their laptop, and one of those edits vanishes without a trace. That’s not a bug report you’ll enjoy.

CRDTs — conflict-free replicated data types — solve the general case elegantly. They let independent replicas converge to the same state without coordination. But they cost you in storage, in conceptual overhead, and in the size of the library you ship. For a collaborative text editor, they’re worth every byte. For a settings screen, they’re a cannon aimed at a fly.

So I think about it on a spectrum. A lot of apps don’t need the general case:

  • Per-document ownership — one writer at a time per object — sidesteps most conflicts without any fancy machinery.
  • Append-only logs turn “edits” into “events,” and events don’t conflict; they just interleave.
  • Coarse locking is unfashionable but perfectly fine when concurrent edits are rare and the cost of a conflict is high.

Each of these gets you most of the local-first benefit for a fraction of the weight. The art is matching the mechanism to how the data is actually used, not to how impressive the mechanism sounds.

When I reach for it

I don’t reach for local-first on everything. A reporting dashboard that reads from a warehouse has no business pretending the data lives on your laptop. A banking transaction wants the server to be the authority, full stop. The model earns its complexity when the user is creating and editing — notes, tasks, documents, drawings, the things people feel ownership over.

But every time it fits, the app ends up faster, calmer, and more honest about what it actually needs the network for. The interface stops apologizing for the network’s bad days. That’s a trade I’ll keep making.