Local-First Software: Why the Future of Apps Doesn't Need the Cloud
Your apps break when the internet goes down. They break when the company shuts down. They break when someone decides to change the API. What if they didn't?
There's a quiet revolution happening in software architecture, and it's going in the opposite direction of everything the industry has been pushing for the last decade. While we've been cramming everything into cloud microservices and SaaS platforms, a growing movement of developers has been asking a deceptively simple question: what if the app just worked, on your machine, with or without the internet?
Welcome to local-first software.
What Is Local-First, Actually?
Local-first software is an architectural philosophy where the primary copy of your data lives on your device — not on a remote server. The cloud becomes a synchronization and backup layer, not a dependency.
The term was popularized by Ink & Switch in their influential 2019 essay, but the ideas have been brewing much longer. The core principles:
- No loading spinners — the app works instantly because data is local
- Works offline — full functionality without internet
- Data longevity — your files outlive the company that made the app
- Privacy by default — your data isn't sitting on someone else's server
- Real-time collaboration — when you are online, changes sync seamlessly
If you've used Figma, you've experienced this. If you've used Linear, Obsidian, or Logseq, you've felt the difference. The app feels different when it doesn't need to round-trip to a server for every keystroke.
The Technical Foundation: CRDTs
None of this works without a key technical innovation: Conflict-free Replicated Data Types (CRDTs).
In traditional collaborative apps (Google Docs, for example), you need a central server to resolve conflicts. Two people edit the same paragraph? The server decides who wins. That means everything depends on the server being available.
CRDTs flip this. They're data structures designed so that any two copies can be merged, in any order, and they'll always converge to the same result. No central arbiter needed.
// Simplified example: a CRDT-based counter
// Two people can increment independently,
// and the merge is just addition — commutative and associative.
class GCounter {
constructor(nodeId) {
this.nodeId = nodeId;
this.counts = { [nodeId]: 0 };
}
increment() {
this.counts[this.nodeId]++;
}
value() {
return Object.values(this.counts).reduce((a, b) => a + b, 0);
}
merge(other) {
for (const [node, count] of Object.entries(other.counts)) {
this.counts[node] = Math.max(this.counts[node] || 0, count);
}
}
}
This is the simplest CRDT. Real ones are far more complex — automerge and Yjs handle rich text, trees, and arbitrary JSON. But the principle holds: merge without conflicts, no server required.
Why Now?
Local-first isn't new. Before the cloud era, all software was local-first. Your Word documents lived on your hard drive. But three things have converged to make a new generation of local-first software viable:
1. Browser Storage Got Serious
IndexedDB, the File System Access API, and OPFS (Origin Private File System) now give web apps real, fast, persistent storage. You can store gigabytes locally in a browser tab. That wasn't possible ten years ago.
2. CRDTs Matured
Libraries like Automerge and Yjs have gone from academic curiosities to production-ready tools. They handle edge cases that would have made local-first apps unreliable five years ago.
3. Users Are Tired of SaaS Lock-in
Every year, beloved apps shut down or enshittify. Users lose access to their data, their workflows, their communities. Local-first offers an antidote: if the data is on your device, the death of a company is an inconvenience, not a catastrophe.
The Architecture in Practice
Here's what a typical local-first stack looks like in 2026:
┌─────────────────────────────────────────┐
│ Client Device │
│ ┌─────────┐ ┌──────────┐ ┌────────┐ │
│ │ App │──│ Local │──│ Sync │ │
│ │ (UI) │ │ CRDT DB │ │ Engine │ │
│ └─────────┘ └──────────┘ └───┬────┘ │
│ │ │
└──────────────────────────────────┼──────┘
│ WebSocket / HTTP
┌───────┴───────┐
│ Sync Server │
│ (optional) │
│ - P2P relay │
│ - Backup │
│ - Auth │
└───────────────┘
The key insight: the sync server is optional. The app works fine without it. The server exists for convenience — cross-device sync, sharing with others, backup — not as a hard dependency.
Real Projects Doing This Right Now
- Linear — Project management with a local-first architecture. Blazingly fast because nothing waits for a server.
- Obsidian — Notes stored as local Markdown files. The "sync" is optional. The data is always yours.
- TinyBase — A reactive local-first data store for apps. Open source, CRDT-backed.
- Electric SQL — Postgres sync engine. Brings local-first to existing databases.
- Triplit — Full-stack local-first framework with real-time sync built in.
The Hard Parts
Let's be honest: local-first isn't a silver bullet. There are genuine challenges.
Authorization is tricky. When data lives everywhere, how do you enforce access control? You can't just check permissions at the API layer — the data is already out there. Solutions exist (encrypted CRDTs, capabilities-based access), but they're complex.
Storage is limited on mobile. A desktop app can store gigabytes. A mobile browser has constraints. This is improving, but it's a real consideration.
Large datasets are hard to sync. If your app has terabytes of data, you can't just replicate everything everywhere. You need selective sync, partial replicas, and lazy loading — which adds significant complexity.
Debugging is harder. When you have distributed state across N devices with eventual consistency, reproducing bugs is a nightmare. Tooling is catching up, but it's not there yet.
A Minimal Example with Yjs
If you want to start building local-first today, Yjs is probably the fastest path:
import * as Y from 'yjs';
import { WebrtcProvider } from 'y-webrtc';
// Create a shared document
const doc = new Y.Doc();
// Define a shared array
const todos = doc.getArray('todos');
// Add items — works offline
todos.push([new Y.Map([['text', 'Learn CRDTs'], ['done', false]])]);
// Connect to peers when online
const provider = new WebrtcProvider('my-room', doc);
// Listen for remote changes
todos.observe(event => {
console.log('Todos updated:', todos.toArray());
});
That's it. You have a collaborative todo list with offline support in ~15 lines. No server needed — WebRTC handles peer-to-peer connections directly.
Where This Is Heading
The local-first movement is at an inflection point. We're moving from niche tools to foundational infrastructure. Expect to see:
- More frameworks abstracting away CRDT complexity (Electric, Triplit, and others leading the charge)
- Browser APIs improving — the File System Access API is still Chrome-only; cross-browser support will unlock more possibilities
- Hybrid architectures becoming the norm — cloud for compute-intensive tasks, local for everything else
- AI models running locally combining with local-first data — your assistant operating on your device, on your data, with full offline capability
The pendulum is swinging back from "everything in the cloud" toward "your data, your device, your control." Not because the cloud is bad, but because depending on it for everything was always an overcorrection.
The Bottom Line
Local-first software is how software should have always worked: fast, reliable, and respecting your ownership of your data. The cloud is a feature, not a prerequisite.
If you're building a new app in 2026, ask yourself: does this really need to phone home on every click? The answer is probably no.
The best software is the software that works when everything else doesn't.
Top comments (0)