Local-First Software: A Practical Guide for Web Developers
The web was built on a client-server model. Your browser sends a request, a server processes it, and a response comes back. This model has served us well for decades, but it comes with trade-offs that we have largely accepted as inevitable: loading spinners, offline errors, and the uncomfortable reality that your data lives on someone else’s computer.
Local-first software challenges that assumption. It puts the user’s device at the centre, treating the server as a convenient sync layer rather than the single source of truth. The result is software that is faster, more resilient, and gives users genuine ownership of their data. The Ink & Switch research paper on local-first software ↗ first articulated these principles in 2019, and the ecosystem has matured dramatically since then.
Why Local-First Matters Now
Three developments have made local-first practical for mainstream web development in 2026.
First, browser storage APIs have matured. IndexedDB, the Origin Private File System, and the Storage API now provide reliable, high-capacity local storage. The days of being limited to 5MB in localStorage are long gone. Modern browsers can store gigabytes of structured data locally, making it feasible to keep a full working copy of application data on the device. The MDN Storage API documentation ↗ details the current capabilities and quota management.
Second, the CRDT ecosystem has reached production quality. Libraries like Yjs ↗ and Automerge ↗ provide battle-tested implementations of conflict-free replicated data types that handle the hardest part of local-first: merging concurrent edits without data loss.
Third, sync engines have emerged as a distinct product category. Tools like ElectricSQL ↗ and PowerSync ↗ bridge the gap between local databases and server-side PostgreSQL, handling bidirectional sync so you do not have to build it yourself.
The Core Principles
The Ink & Switch paper ↗ defines seven ideals for local-first software. Understanding these helps you evaluate how far to take the pattern in your own projects.
| Principle | What It Means | Practical Impact |
|---|---|---|
| No spinners | Data reads are instant from local storage | UI renders immediately, no loading states for cached data |
| Your work is not trapped | Data is stored in open formats on the user’s device | Users can export, back up, and migrate freely |
| The network is optional | Full functionality without an internet connection | Works on aeroplanes, in basements, and in rural areas |
| Seamless collaboration | Multiple users can edit simultaneously | Changes merge automatically via CRDTs |
| The Long Now | Data persists for decades, not until a startup shuts down | No vendor lock-in to a specific cloud service |
| Security and privacy by default | End-to-end encryption is natural | Data stays encrypted until it reaches an authorised device |
| User retains ownership | No terms of service can revoke access | Users control their own data completely |
Not every application needs all seven. Most teams adopt local-first principles selectively, focusing on the ones that deliver the most value for their use case.
How CRDTs Solve the Hard Problem
The fundamental challenge of local-first software is conflict resolution. When two users edit the same document while offline, what happens when they reconnect? Traditional approaches (last-write-wins, manual merge) are either lossy or require user intervention.
CRDTs solve this mathematically. They are data structures designed so that any two replicas can be merged deterministically, regardless of the order in which edits arrived. The result is always consistent across all devices, with no data loss and no user intervention.
There are two main families of CRDTs you will encounter:
Operation-Based CRDTs (CmRDTs)
These transmit individual operations (e.g. “insert character ‘a’ at position 5”) between replicas. They produce smaller messages but require a reliable, ordered delivery channel. Yjs uses this approach.
State-Based CRDTs (CvRDTs)
These transmit the full state of the data structure and merge it using a defined merge function. They are simpler to implement and tolerant of unreliable networks, but produce larger messages. Automerge uses a hybrid approach that combines the strengths of both.
For most web applications, you do not need to implement CRDTs from scratch. The CRDT.tech resource hub ↗ provides an excellent overview of the academic foundations, but in practice you will work with a library that handles the details.
Choosing the Right Tools
The local-first ecosystem has grown rapidly. Here is a comparison of the most mature options for web developers.
| Tool | Approach | Best For | Language | Storage |
|---|---|---|---|---|
| Yjs | CRDT library | Real-time collaboration, text editing | JavaScript/TypeScript | Pluggable (IndexedDB, etc.) |
| Automerge | CRDT library | Document-based apps, JSON-like data | Rust core, JS bindings | Pluggable |
| ElectricSQL | Sync engine | Apps with existing PostgreSQL databases | TypeScript | SQLite (local), PostgreSQL (server) |
| PowerSync | Sync engine | Mobile and web apps needing SQL locally | TypeScript, Dart | SQLite (local), PostgreSQL (server) |
| RxDB | Reactive database | Apps needing real-time queries and sync | TypeScript | IndexedDB, SQLite, etc. |
When to Use a CRDT Library
Choose Yjs or Automerge when you need fine-grained collaboration (like Google Docs-style editing), when your data model is document-shaped, or when you want full control over your sync layer. These libraries give you the building blocks and let you assemble them however you like.
When to Use a Sync Engine
Choose ElectricSQL or PowerSync when you have an existing PostgreSQL database and want to sync subsets of that data to client devices. Sync engines handle the bidirectional replication, conflict resolution, and permission model so you can focus on your application logic. This approach works particularly well when your team already understands relational data modelling, which is something we covered in how to choose the right database.
Building Your First Local-First Feature
You do not need to rebuild your entire application. A pragmatic starting point is to add local-first behaviour to a single feature. Here is a pattern that works well.
Step 1: Identify a Feature That Benefits From Offline Support
Look for features where users create or edit data and would benefit from instant feedback. Note-taking, form entry, task management, and drawing tools are all excellent candidates.
Step 2: Add a Local Database Layer
Use IndexedDB (directly or via a wrapper like RxDB ↗) to store data locally. Your application reads from and writes to this local store, not directly to your API. The MDN IndexedDB documentation ↗ covers the API in detail.
// Simplified example: writing to a local store first
async function saveNote(note: Note) {
// Write to local database immediately
await localDB.put('notes', note);
// Queue a sync operation for when connectivity is available
syncQueue.push({ type: 'upsert', collection: 'notes', data: note });
// Attempt to sync immediately if online
if (navigator.onLine) {
await syncToServer();
}
}
Step 3: Implement a Sync Queue
Build a background sync process that pushes local changes to your server when connectivity is available. Use the online and offline events, combined with the Background Sync API where supported, to trigger sync attempts.
// Listen for connectivity changes
window.addEventListener('online', async () => {
await syncToServer();
});
// Process queued operations
async function syncToServer() {
const pending = await syncQueue.getAll();
for (const operation of pending) {
try {
await api.sync(operation);
await syncQueue.remove(operation.id);
} catch (error) {
// Retry on next connectivity event
console.warn('Sync failed, will retry:', error);
break;
}
}
}
Step 4: Handle Conflicts
For simple cases, a last-write-wins strategy with timestamps may be sufficient. For more complex scenarios, integrate a CRDT library. The key is to decide on your conflict resolution strategy early, because it affects your data model.
This incremental approach lets you adopt local-first principles without a full rewrite. It pairs naturally with the resilience patterns described in building resilient APIs, since your sync layer needs the same retry and backoff logic.
Architecture Patterns for Local-First Web Apps
There are three common architectural patterns, each suited to different levels of complexity.
Pattern 1: Local Cache with Server Sync
The simplest approach. Your server remains the source of truth, but you cache data locally and write to the cache first. This gives you instant UI updates and basic offline support. Conflicts are resolved server-side, typically with last-write-wins.
This is not truly local-first by the strict definition, but it captures most of the UX benefits and is the easiest to adopt incrementally.
Pattern 2: CRDT-Backed Documents
Each document (or entity) is backed by a CRDT. Edits are applied to the local CRDT instance and synced to other devices via a relay server. The server does not need to understand the data structure; it simply forwards CRDT operations between clients.
This pattern works brilliantly for collaborative editing and is what powers tools like Figma’s multiplayer features. It requires more upfront investment but delivers true conflict-free collaboration.
Pattern 3: Local SQLite with Server Replication
Your application runs a full SQLite database on the client (via WebAssembly) and syncs it with a server-side PostgreSQL database. ElectricSQL and PowerSync both implement this pattern. It gives you the full power of SQL locally, which is a compelling advantage if your team already thinks in relational terms.
Understanding the trade-offs between these patterns connects directly to broader architectural thinking. For more on evaluating architectural decisions, see the case for boring technology and the pragmatic approach to microservices.
Common Pitfalls to Avoid
Local-first development introduces challenges that server-centric developers may not have encountered before.
Storage Quotas
Browsers impose storage limits, and they vary significantly. Safari is historically the most restrictive. Always implement a storage management strategy that monitors usage, cleans up stale data, and degrades gracefully when limits are reached.
Large Data Sets
Not all data belongs on the client. If your application manages millions of records, you will need a selective sync strategy that only replicates relevant subsets to each device. Sync engines like ElectricSQL handle this with shape-based subscriptions.
Authentication and Permissions
When data lives on the client, you cannot rely on server-side authorisation to control access. You need a permission model that works at the sync layer, ensuring that clients only receive data they are authorised to see. This is a fundamentally different approach from traditional authentication patterns, and it deserves careful design.
Testing
Testing local-first applications is more complex than testing server-rendered apps. You need to simulate offline scenarios, concurrent edits, sync failures, and storage quota exhaustion. Invest in your testing infrastructure early.
Performance Benefits Are Substantial
The performance argument for local-first is compelling. When your application reads from a local database instead of making network requests, the difference is not incremental; it is transformational.
Local reads consistently complete in under 10 milliseconds regardless of network conditions. For users on slow connections, in rural areas, or simply on a train going through a tunnel, this is the difference between a usable application and a broken one. These gains complement web performance optimisations at the network and rendering layer.
Getting Started Today
You do not need to adopt every local-first principle at once. Here is a practical roadmap.
-
Start with read caching. Cache API responses in IndexedDB and serve them instantly on subsequent visits. This alone eliminates loading spinners for returning users.
-
Add optimistic writes. Write to the local store first and sync to the server in the background. This makes your UI feel instant, even on slow connections.
-
Implement a sync queue. Build a reliable queue that retries failed sync operations with exponential backoff. This handles intermittent connectivity gracefully.
-
Evaluate CRDTs for collaborative features. If your application involves multiple users editing the same data, explore Yjs or Automerge. The learning curve is manageable, and the libraries handle the complex merge logic for you.
-
Consider a sync engine for data-heavy apps. If you need SQL queries on the client or bidirectional sync with PostgreSQL, evaluate ElectricSQL or PowerSync.
The local-first approach represents a meaningful shift in how we think about web architecture. It is not about abandoning servers; it is about building software that treats the network as an enhancement rather than a requirement. As the tooling continues to mature, the cost of adopting these patterns will only decrease, while the user experience benefits remain compelling. For teams already thinking about event-driven architecture, local-first adds a natural extension: events that originate on the device and propagate outward, rather than flowing exclusively from server to client.
Frequently asked questions
What is local-first software?
Local-first software stores data primarily on the user's device and treats the server as a secondary sync layer rather than the source of truth. This means the application works fully offline, responds instantly to user input without waiting for network round trips, and gives users complete ownership of their data. When connectivity is available, changes sync automatically with other devices or collaborators.
How is local-first different from offline-first?
Offline-first typically means caching server data locally so the app can function without a connection, but the server remains the source of truth. Local-first goes further: the local copy is the primary copy. The app is designed to work indefinitely without a server. Sync is a feature, not a requirement. This distinction affects everything from data modelling to conflict resolution strategy.
What are CRDTs and why do they matter for local-first apps?
CRDTs (Conflict-free Replicated Data Types) are data structures that can be modified independently on multiple devices and merged together without conflicts. They guarantee that all replicas eventually converge to the same state, regardless of the order in which changes arrive. This makes them ideal for local-first apps where multiple users or devices may edit the same data without coordinating through a central server.
Do I need to understand CRDTs deeply to build local-first apps?
Not necessarily. Libraries like Yjs and Automerge abstract away the CRDT internals and expose simple APIs for working with shared documents. You can build a fully functional local-first app using these libraries without understanding the mathematical foundations. However, a basic grasp of how CRDTs resolve conflicts will help you design better data models and debug sync issues.
Is local-first suitable for every type of application?
No. Local-first works best for applications where users create and edit data collaboratively or across devices, such as note-taking apps, document editors, project management tools, and creative software. Applications that rely heavily on server-side computation, real-time authoritative state (like multiplayer games with anti-cheat), or large datasets that cannot fit on a device are less suited to a purely local-first approach. That said, most apps can benefit from local-first principles, even if they do not adopt the full pattern.
Enjoyed this article? Get more developer tips straight to your inbox.
Comments
Join the conversation. Share your experience or ask a question below.
No comments yet. Be the first to share your thoughts.