
Local-first software is where it's at
Your data should be yours, not someone else's. Join the movement!
Happy Easter
Post-read, you absolutely should not go and re-write all of your apps with CRDTs, Yjs, and IndexedDB.
This is only a thought-piece on why this stuff is wonderful.
After all, this blog is a series of random thoughts.
Today, more than ever before - we are dependent on the cloud. Whether it's our GPUs, our music, our photos, our documents, or our emails, I can go forever. No matter how I put it, the reality is that this dependency cuts deep.
Sadly, it only becomes apparent when that same dependency is taken away. Like an ipad being prohibited from a toddler, many of us - especially businesses - would struggle to function without our cloud fix.

Just one week without access to cloud services would bring countless people to their knees. And IMO, that's just not a good place to be. While a dissenting view could argue that the cloud is a "necessary evil" - I'd argue that evil should never be the default for a neccessity.
What's going on is that we're trading our ownership for convenience. Our data is in one moment, on our devices - yet another, in some distant server.
As a community, (if you haven't noticed) we're considering a different approach. And that approach is local-first.
On this Page
The (short) history
Back in 2019, Ink & Switch published one of my all-time favorite essays, sparking a movement that's still gaining momentum today. In the article, they propose "local-first software": a set of principles that enables both collaboration and ownership for users.
If you're like me, you remember when software lived primarily on our machines. We called the shots, had offline access, and felt that sense of true ownership. But, let's be honest - sharing and collaborating with others wasn't exactly easy.

For the most part, cloud apps solved that problem, providing us with both centralized and distributed services that solve multiple problems pretty well. But in doing so, they often sacrificed the ownership and privacy that made it feel like we were in control.
Local-first software aims to bridge this gap, giving us the best of both worlds. Not only is it wicked fast, but it also works offline (without internet connection), brings back private storage, and most importantly, puts all of us back in control of our data.
Pipe dream? To me, nahhhhh never!
"Local-first"
In my view, when something is "local-first" - it simply means that the (sync engine, application, CRDT, etc.) prioritizes local storage and local networks over remote servers.

In local-first:
- Scaling is done from the client outward, not server downward.
- Data is stored locally — on your device, in your browser and in some cases, if you don't feel completely ready for that, a (remote) file server.
- We still collaborate — but via CRDTs or peer-to-peer networks instead of a "centralized" server (in some cases, still used for syncing).
Your data lives primarily on your device, not on someone else's computer. This has SERIOUS implications:
- Your data isn't sitting on a centralized server, making it less of a target for hackers, the FBI, or your boss.
- Speed! Local data access means near-instantaneous response times.
- You own your files and can do with them as you please.
- Airplane mode? No problem. Keep working, and sync your changes later.
Don't confuse "local-first" with "local-only." The goal isn't to eliminate networking entirely, but to prioritize local operations while still enabling collaboration.
The Seven Ideals
The folks at Ink & Switch (and others in the space) have outlined some key principles for local-first software:
- No Spinners
- Your Work is Not Trapped
- Network Optional
- Seamless Collaboration
- The Long Now
- Security and Privacy by Default
- You Retain Ultimate Ownership and Control
You may be wondering what these semantic meanings are, but they're not important right now. What is important is that these are a set of principles that guide the design of local-first software. More details here.
CRDTs
It's not obvious how these systems achieve this "collaborative utopia" without constant conflicts. The answer lies in Conflict-free Replicated Data Types (CRDTs). These clever data structures allow multiple users to modify data concurrently, and the changes are automatically merged without conflicts.
At the core of many CRDTs is the idea of tracking your data plus some metadata, then automatically merging changes without ever losing updates. One of the simplest examples is the Last-Writer-Wins (LWW) Register, which keeps:
value
: the current datatimestamp
: when that data was writtennodeId
: which client/node wrote it
When two registers are merged, the one with the newest timestamp (or higher nodeId
if timestamps tie) wins.
1class LWWRegister {2 constructor(3 value = null,4 timestamp = 0,5 nodeId = generateNodeId()6 ) {7 this.value = value8 this.timestamp = timestamp9 this.nodeId = nodeId10 }11
12 setValue(newValue: any) {13 const now = Date.now()14 if (now > this.timestamp) {15 this.value = newValue16 this.timestamp = now17 }18 return this19 }20
21 merge(other: LWWRegister) {22 const shouldUpdate =23 other.timestamp > this.timestamp ||24 (other.timestamp === this.timestamp && other.nodeId > this.nodeId)25
26 if (shouldUpdate) {27 this.value = other.value28 this.timestamp = other.timestamp29 this.nodeId = other.nodeId30 }31 return this32 }33}
Once you have the class, you can create and merge registers like so:
1const regA = new LWWRegister("hello")2const regB = new LWWRegister("world")3
4regA.setValue("hi")5regB.setValue("earth")6
7// merge regB into regA; the later write wins8regA.merge(regB)9console.log(regA.value) // "earth" if regB was updated more recently
This LWW Register is only the tip of the iceberg. More advanced CRDTs let you work with lists, maps, sets, and even collaboratively edited rich‑text documents—always conflict‑free.
CRDTs aren't the only way to achieve a local-first approach. We can also do other techniques, like Operational Transformations, which are a way to modify some state such that it can be applied in a distributed system.

A byte-sized example
Let's walk through a small example of a local-first note-taking app using Yjs, demonstrating how to combine CRDTs, client-side persistence, and peer-to-peer networking into a fully functional app.
1. Install dependencies
1bun add yjs y-indexeddb y-webrtc
2. Create a React hook for the CRDT document
1import * as Y from 'yjs'2import { WebrtcProvider } from 'y-webrtc'3import { useEffect, useState } from 'react'4import { IndexeddbPersistence } from 'y-indexeddb'5
6export function useLocalFirstDoc(roomName: string) {7 const [doc] = useState(() => new Y.Doc())8
9 useEffect(() => {10 // persist changes to IndexedDB, enable offline support11 const persistence = new IndexeddbPersistence(roomName, doc)12 // enable peer-to-peer sync via WebRTC (optional)13 const provider = new WebrtcProvider(roomName, doc)14
15 return () => {16 provider.destroy()17 persistence.destroy()18 }19 }, [roomName, doc])20
21 return doc22}
3. Apply the hook in our stub component
1import { useState, useEffect } from 'react'2import { useLocalFirstDoc } from '../hooks/useLocalFirstDoc'3
4function Notes() {5 const doc = useLocalFirstDoc('nyuma-notes')6 const [notes, setNotes] = useState<string[]>([])7
8 useEffect(() => {9 const yarray = doc.getArray<string>('notes')10 setNotes(yarray.toArray())11 yarray.observe(() => setNotes(yarray.toArray()))12 }, [doc])13
14 function addNote(text: string) {15 const yarray = doc.getArray<string>('notes')16 yarray.push([text])17 }18
19 return (20 <div>21 <ul>22 {notes.map((note, i) => (<li key={i}>{note}</li>))}23 </ul>24 <button onClick={() => addNote(prompt('New note') || '')}>25 Add Note26 </button>27 </div>28 )29}
Now, when we run this, we get a simple note-taking app that persists to IndexedDB, syncs via WebRTC, and resolves conflicts via CRDTs. Each time a client makes a change, the CRDT engine automatically merges the changes, and the app updates in real-time. Super easy!
You can swap out y-webrtc
for other providers (e.g., WebSocket-based or server-backed) to create hybrid sync models. The same also applies to the persistence layer - Dexie, PouchDB, etc.
Should you care?
If you value some of the things I've mentioned above, local-first software is definitely something to keep an eye on. It represents a shift in power, putting humans back in charge of their digital lives.
While it's not going to replace all cloud applications overnight, I atleast HOPE it offers a compelling alternative for specific use cases, especially those involving creative work, personal data management, and situations where reliable internet access is not guaranteed.
A note on the future
The transition to a cloud-dominated world has brought undeniable benefits, but it's also important to recognize what we've lost along the way. Local-first software offers a path towards a more balanced future, one where we can enjoy the convenience of the cloud without sacrificing ownership, privacy, or performance.
So, next time you're frustrated by a slow-loading cloud app, take a moment to consider the alternative. Maybe, just maybe, local-first software is where it's at. And happy 4/20. Go spark some creativity (responsibly, of course)!
This post was inspired by the groundbreaking work of the Ink & Switch research lab and their Local-First Software paper. Shoutout to Slim for exposing me to this stuff at Notion.