Loro Extended, or, Why I get excited about CRDTs
A deep dive into distributed systems, local-first software, and the future of collaborative applications. A mock interview about Loro Extended, the state sync typescript library.
The "Why" Behind the Work
1. The Origin Story
Q: You've spent half a year on this. What was the specific "Aha!" moment--or maybe the "I'm going to throw my computer out the window" moment--with existing sync solutions that convinced you Loro Extended needed to exist?
The origin story actually goes back further than six months. I built Relm, a 3D world in a browser, for three years on Yjs and learned a lot about CRDTs and state synchronization in that time. It was a great lesson in what works, what breaks, and where the sharp edges are.
I've since worked at a handful of education companies, where students learning together is a key component of pedagogy. This means the type of app we needed to build involved multiple devices and several "players." When we needed to synchronize state for student groups, my frustration with the status quo web frameworks crystallized. I kept seeing the same pattern: RESTful endpoints are ubiquitous, and take a lot of time to build. Easily half of application code is plumbing--shuttling data between client and server, handling loading states, managing cache invalidation, and dealing with optimistic updates that fail.
And I've seen many engineers keep re-encountering the same edge cases: real-time sync issues e.g. via socket.io, multiple tabs fighting over state, undo/redo that doesn't quite work, offline hiccups that lose user data, browser storage that mysteriously disappears. Most teams solve these problems in ad-hoc ways, and very few approach them systematically or rigorously.
The "aha" moment was realizing that CRDT technology is not just a "collaborative editing" solution--it's the next foundation technology for web application development. It can replace fundamental layers of our stack like REST, RPC, and in some cases, even databases. We've been thinking about CRDTs too narrowly. As an example--if documents are cheap, why not use one to connect a single client with the server? Or treat a single user's multiple devices as "multiplayer" and solve a wide swath of problems all in one pass?
So Loro Extended exists to make that vision practical. Not just "here's a CRDT library," but "here's a complete toolkit for building applications where state synchronization is a solved problem, not a constant battle."
2. The "Why Now"
Q: CRDTs have been around for a while. Why do you think 2026 is the turning point where they transition from "academic curiosity" to "foundational tech" for every TypeScript developer?
Three things have converged to make this the moment:
First, the performance problem is solved. Early CRDTs had real performance issues--memory bloat, slow merges, documents that grew unboundedly. Loro, the Rust-based CRDT engine we build on, has cracked this. It's incredibly fast at merging and calculating diffs, capable of handling large documents and long editing sessions that would slow other libraries to a crawl. It even supports shallow snapshots and redaction, so you're not tied to the entire document history forever. The "CRDTs are too slow" objection is simply no longer true.
Second, developer experience has matured. Raw CRDTs feel like "schemaless JSON soup." You're working with containers and operations, not your domain model. Loro Extended brings structure back with schema-first design, full TypeScript inference, and natural JavaScript syntax. You write `doc.todos.push({ text: "Buy milk" })` instead of verbose CRDT operations. The gap between "how I think about my data" and "how I write code" has closed.
Third, the ecosystem is ready. We have WebSocket, WebRTC, Server-Sent Events, IndexedDB, PostgreSQL--all the transport and storage primitives are mature. What was missing was the glue layer that makes them work together seamlessly. That's what Loro Extended provides: adapters that let you mix and match network and storage backends without changing your application code.
The timing is also cultural. I think maybe developers are exhausted by the complexity of modern web stacks. They're looking for simpler mental models. "Mutate local state, sync happens automatically" is a much simpler model than "make API call, handle loading state, handle error state, handle cache invalidation, handle optimistic updates, handle conflicts..."
Performance & Technical Deep Dive
3. The "Loro" Foundation
Q: You chose to build on top of Loro (the Rust-based CRDT framework). For those who aren't familiar, what does Loro provide as a substrate, and what does the "Extended" layer specifically add for the TypeScript ecosystem?
Think of it as Loro is the engine, Loro Extended is the car.
Loro gives you the core CRDT primitives: LoroMap, LoroList, LoroText, LoroTree, LoroCounter, and the merging algorithms that make them convergent. It's written in Rust and compiled to WebAssembly, so it's blazingly fast. It handles the hard mathematical guarantees--when two users edit the same document simultaneously, Loro ensures they converge to the same state without coordination.
Loro Extended adds everything you need to actually build an application:
Schemas and Type Safety. Define your document structure with Shape builders and get full TypeScript inference. No more doc.getMap("root").get("todos") casting to any.
(aside, here's an example for readers:)
const schema = Shape.doc({
todos: Shape.list(Shape.plain.struct({
text: Shape.plain.string(),
done: Shape.plain.boolean(),
})),
});
Network Synchronization. Adapters for SSE, WebSocket, WebRTC, HTTP polling. The sync protocol handles server/client, or peer discovery, as well as message exchange and merging.
Storage Persistence. Adapters for IndexedDB (browser), LevelDB (Node.js), and PostgreSQL. Your documents persist locally and sync when connectivity returns.
React/Hono Hooks. useHandle, useDoc, useEphemeral, useCollaborativeText--reactive bindings that make your UI update automatically when the document changes.
Presence System. Share ephemeral state like cursors, selections, and user status. It's not stored in the CRDT--it's broadcast in real-time and cleaned up when users disconnect.
Placeholders (Empty State Overlay). Default values that appear when CRDT containers are empty, without causing initialization race conditions between peers.
The philosophy is "zero plumbing." You define your schema, wire up adapters, and focus on your application logic. The sync just works.
4. The Latency War
Q: Developers are worried that CRDTs will bloat their memory or slow down their UI. How does Loro Extended handle heavy state without making the browser sweat?
This fear is legitimate for older CRDT implementations, but Loro has fundamentally solved it. Let me explain how:
Rust + WebAssembly. The core CRDT operations happen in compiled Rust code, not JavaScript. Merging, diffing, and serialization are orders of magnitude faster than pure JS implementations.
Efficient Binary Format. Loro uses a compact binary encoding for document snapshots and updates. You're not shipping JSON over the wire--you're shipping optimized binary deltas.
Shallow Snapshots. This is huge. Traditional CRDTs keep the entire operation history, forever. Loro supports "shallow snapshots" where you can trim history while preserving the ability to merge recent participants. Your documents don't grow unboundedly.
Lazy Container Creation. In Loro Extended, containers are only created when accessed. If your schema has 50 fields but you only use 5, you're not paying for the other 45.
Batched Mutations. The change() function batches multiple mutations into a single transaction. Instead of committing after every keystroke, you commit once per logical operation.
Fine-Grained Reactivity. Our React hooks support selectors. Instead of re-rendering your entire component when any part of the document changes, you can subscribe to just doc.todos.length and only re-render when that specific value changes.
Placeholder Computation. Default values aren't stored in the CRDT--they're computed on read. Zero storage overhead for defaults.
In practice, we've seen Loro handle documents with tens of thousands of operations without perceptible lag. The "CRDTs are slow" reputation comes from earlier implementations that didn't have these optimizations.
5. Schema Evolution
Q: One of the hardest parts of long-lived sync is when the data model changes. How does Loro Extended handle schema migrations in a world where every client might be on a different version of the app?
This is one of the trickiest problems in distributed systems, and I want to be honest: there's no magic bullet. But Loro Extended's design makes it more manageable than you might expect.
Additive Changes Are Free. Adding new fields to your schema just works. Old documents don't have those fields, so they get placeholder defaults. New clients write to them, old clients ignore them. This covers the majority of real-world schema changes.
Placeholders Enable Graceful Degradation. Because every field can have a placeholder default, your app never crashes on missing data. An old document opened in a new client shows sensible defaults for new fields.
CRDTs Are Inherently Flexible. Unlike SQL databases, CRDTs don't enforce schema at the storage layer. The schema is a TypeScript-level concern. The underlying LoroMap doesn't care if you add a new key.
For Breaking Changes, You Have Options:
- Versioned Document IDs. Instead of migrating `todos-v1`, create `todos-v2` and migrate data at the application layer.
- Lazy Migration. When a client opens an old document, detect the version and transform it. Since CRDTs merge, the migrated structure propagates to other clients.
- Dual-Write Period. Write to both old and new fields during a transition period, then deprecate the old fields.
The key insight is that CRDT schema evolution is more like NoSQL schema evolution than SQL migrations. You're not running ALTER TABLE--you're evolving a flexible document structure over time.
We're also exploring more sophisticated migration tooling for future versions, but the current approach handles most real-world scenarios.
Practicality & DX (Developer Experience)
6. "Practical" Sync
Q: You use the word "practical" in your mission statement. What's one thing that is traditionally a nightmare to do with CRDTs--like undo/redo or partial loading--that your library makes trivial?
Let me give you two examples that I think really showcase the "practical" philosophy:
Example 1: Instant Load with No Spinners
Traditional apps have this pattern everywhere:
if (loading) return <Spinner />;
if (error) return <Error />;
return <Content data={data} />;
With Loro Extended, documents are immediately available:
const handle = useHandle("my-doc", schema);
const doc = useDoc(handle);
return <Content data={doc} />; // Always renders, never spins
How? Placeholders. Your schema defines default values, and those defaults are returned instantly while the real data loads in the background. When the data arrives, it merges seamlessly. No loading states, no spinners (unless you want them), no flash of empty content.
This sounds simple, but it's transformative for UX. Your app feels instant.
Example 2: Presence Without Infrastructure
In a traditional app, adding "show who's online" or "show other users' cursors" requires:
- A separate WebSocket connection for presence
- Server-side presence tracking
- Heartbeat logic to detect disconnects
- Client-side state management for peer data
With Loro Extended:
const handle = useHandle("doc", DocSchema, { presence: PresenceSchema });
// Set your presence
handle.presence.setSelf({ cursor: { x: 100, y: 200 }, name: "Alice" });
// Read others' presence
const { peers } = useEphemeral(handle.presence);
That's it. Presence piggybacks on your existing sync connection. Disconnects are detected automatically. Peer data is cleaned up when they leave. You didn't write any infrastructure code.
These aren't flashy features--they're the "death by a thousand cuts" problems that eat up developer time. Making them trivial is what "practical" means.
7. The React/Frontend Bridge
Q: Most of our listeners are building in React, Vue, or Svelte. How does Loro Extended bridge the gap between "complex CRDT operations" and "simple reactive state" that feels native to these frameworks?
The key insight is that CRDTs and reactive frameworks actually have a lot in common--they're both about "state changes trigger updates." The challenge is bridging the impedance mismatch.
Here's how we do it for React (and the pattern applies to other frameworks):
Stable Handles, Reactive Values. The useHandle hook returns a stable reference that never changes. This prevents unnecessary re-renders. The useDoc hook subscribes to changes and triggers re-renders only when the document actually changes.
const handle = useHandle("doc", schema); // Never re-renders
const doc = useDoc(handle); // Re-renders on changes
Selectors for Fine-Grained Updates. You can pass a selector to useDoc to subscribe to just part of the document:
const todoCount = useDoc(handle, d => d.todos.length);
Now your component only re-renders when the count changes, not when todo text is edited.
Natural Mutation Syntax. Inside handle.change(), you write normal JavaScript:
handle.change(draft => {
draft.todos.push({ text: "New todo", done: false });
draft.todos[0].done = true;
});
No special CRDT operations to learn. It feels like Immer or Zustand.
Unified Document + Presence. The handle gives you both the document and presence in one place:
const handle = useHandle("doc", DocSchema, { presence: PresenceSchema });
const doc = useDoc(handle);
const { self, peers } = useEphemeral(handle.presence);
For Vue and Svelte, the pattern is similar--we provide a `hooks-core` package with framework-agnostic implementations that can be wrapped for any reactive system. The React package is just one binding.
The goal is that using Loro Extended should feel like using any other state management library. The CRDT complexity is hidden behind familiar patterns.
The Big Picture
8. The Collaborative Future
Q: If Loro Extended becomes the standard, how does building a "Google Docs-style" feature change for a solo developer? Are we talking days of work, or minutes?
Let me paint the picture with a concrete example.
Today, without Loro Extended, building collaborative text editing requires:
- Choosing and integrating a CRDT library (Yjs, Automerge, etc.)
- Setting up WebSocket infrastructure for real-time sync
- Implementing a sync protocol (who sends what, when)
- Building presence tracking (cursors, selections)
- Handling offline/online transitions
- Persisting documents (server-side database, client-side IndexedDB)
- Integrating with a rich text editor (ProseMirror, Slate, etc.)
- Handling edge cases (reconnection, conflict resolution, undo/redo)
A solo developer? That's weeks to months of work, and you'll still have bugs.
With Loro Extended, it's a few dozen lines of code.
Add adapters for your network (SSE, WebSocket) and storage (IndexedDB), and you have:
- Real-time collaboration
- Offline support
- Cursor presence
- Undo and redo
- Automatic conflict resolution
- Persistent storage
Minutes, not days. The infrastructure is handled. You focus on your editor UI.
Now, I want to be clear: building a *polished* Google Docs competitor is still a massive undertaking. Rich text editing, comments, suggestions, permissions, sharing--that's product work. But the *sync infrastructure* that used to be the hardest part? That's now a solved problem you can drop in.
9. Beyond Text Editing
Q: Everyone associates CRDTs with text. Can you give us a "wild" use case for Loro Extended that people might not expect? (Gaming? Supply chain? AI agents?)
I love this question because it gets at the real vision. Let me give you three:
1. Multiplayer Games
We have a "Bumper Cars" example in the repo--a multiplayer arena game. The game state (participants, scores) lives in a CRDT. The presence system handles real-time player inputs. The server runs authoritative physics and writes results back to the CRDT.
Why is this interesting? Because the same sync infrastructure that powers a todo app powers a real-time game. You're not building separate systems for "collaborative documents" and "multiplayer games"--it's all state synchronization.
2. AI Agent Cooperation
This is where I get really excited. Think about multi-agent AI systems. Today, agents either:
- Are stateless (suffer from amnesia, need huge context windows)
- Poll a database (slow, race conditions when multiple agents act)
With CRDTs, agents become "peers" in a sync network. An agent subscribes to a document. When a user (or another agent) changes state, the agent gets notified immediately with the precise delta. Multiple agents can work concurrently--one generating code, another fixing bugs, a third updating the UI--without locking or overwriting each other.
If an agent crashes, the state is safe in the CRDT. Another agent picks up exactly where it left off. The state lives in the network, not in any single agent's memory.
3. Distributed State Machines
We have a Shape.tree() type for hierarchical data. Imagine modeling a workflow or state machine as a tree of states. Multiple services can observe and mutate the state machine concurrently. Transitions are conflict-free. You get an audit log for free (CRDT history).
This pattern works for supply chain tracking, approval workflows, IoT device coordination--anywhere you have distributed state that multiple actors need to read and write.
The common thread is: anywhere you have state that multiple entities need to share, CRDTs are a better foundation than request/response APIs. Text editing is just the most obvious example.
The Closing "Hype" Question
10. The 6-Month Mark
Q: You're six months in. What is the most exciting thing you've seen built with Loro Extended so far, and where can people go to break it, test it, and contribute?
The most exciting thing has been seeing the examples come together and realizing how *little code* they require. The video conference example--real-time video with WebRTC, presence for participant status, document sync for shared state--is a few hundred lines of application code. The bumper cars game, same thing. The chat app with AI streaming responses, same thing.
Each example that works is proof that the abstraction is right. You're not fighting the framework; you're expressing your application logic directly.
I'm also excited about the "agentic cooperation" use case. We're just scratching the surface of what's possible when AI agents can share state through CRDTs instead of polling databases.
Where to find us:
- GitHub: The repo is open source. Star it, fork it, file issues.
- Examples: Start with
todo-minimalorhono-counterfor the simplest path. Graduate tovideo-conferenceorbumper-carsfor more complex patterns. - Documentation: The
docs/folder has architecture docs, the getting started guide, and deep dives on specific topics.
How to contribute:
- Try building something and tell us where you got stuck
- File issues for bugs or missing features, discuss and make PRs
- The adapter system is designed for extension--build adapters for your favorite transport or storage
- Documentation improvements are always welcome
We're building the foundation for the next generation of web applications. If you're tired of writing plumbing code, if you've ever lost user data to a sync bug, if you want your apps to work offline and collaborate in real-time--come build with us.
The future is shared state, and it's closer than you think.