Yjs has become the default answer to the question “how do I add real-time collaboration to my app?” The library is fast, well-documented, has a mature provider ecosystem, and the benchmarks are impressive. For a long time, recommending Yjs was uncontroversial. Then Moment.dev published a detailed breakdown of why they don’t use it, and it crystallized something that’s been quietly true for a while: Yjs is excellent at a specific thing, and that thing is not the same as “collaborative editing” in the general sense.
What Yjs Actually Does
Yjs implements YATA (Yet Another Transformation Approach), a CRDT algorithm designed by Kevin Jahns and colleagues. At its core, Yjs maintains a doubly-linked list of items. Each item carries an ID (a client identifier plus a logical clock value), the content it holds, and the IDs of the items to its left and right at the time of insertion. When two clients insert at the same position concurrently, the IDs break ties deterministically. This gives you conflict-free merging without a central coordinator.
The shared types built on top of this structure — Y.Text, Y.Array, Y.Map, Y.XmlElement — are genuinely pleasant to work with:
import * as Y from 'yjs'
import { WebsocketProvider } from 'y-websocket'
const doc = new Y.Doc()
const provider = new WebsocketProvider('wss://your-server', 'my-room', doc)
const text = doc.getText('content')
text.observe(event => {
console.log(text.toString())
})
text.insert(0, 'Hello, world')
Five lines of meaningful collaborative state, no server logic required. The provider handles sync, and the CRDT handles merging. For a prototype or a lightweight use case, this is hard to beat.
The provider model deserves attention here. Yjs separates the document from the transport. y-websocket syncs over WebSockets, y-webrtc uses WebRTC for peer-to-peer, and y-indexeddb persists locally in the browser. You compose these — local persistence plus a server relay, for instance — to build your offline story. The seams between providers are real, but the composability is a genuine design win.
Where the Accumulation Problem Lives
The fundamental issue Moment and others run into is that Yjs never forgets. When you delete content in a Y.Text, the underlying item doesn’t disappear. It becomes a tombstone: a marker that the content existed and was removed. The tombstone stays in the linked list structure permanently, because future insertions might need to reference it as a left or right origin to resolve ordering correctly.
For a collaborative text editor with a bounded document, this is manageable. Your tombstones accumulate, but the document stays finite. For a spreadsheet, a database UI, or any application where users add and delete many rows, columns, or records over months, the internal document structure grows without bound. The binary update log that Yjs encodes reflects this growth. A document with a million past deletions carries that history.
Yjs does have garbage collection. You can enable it with doc.gc = true (it’s the default). But GC in Yjs has a constraint: it can only collect tombstones once it knows that every connected peer has received and processed those deletions. The mechanism for knowing this is the state vector — a map from client ID to clock value that summarizes what each peer has seen. If a peer has been offline for an extended period, or if you have ephemeral clients who connect once and disappear, GC stalls waiting for acknowledgment that will never come.
In practice, applications that use snapshots for time-travel or version history have to disable GC entirely, because GC and snapshots are incompatible. If you want to restore a document to a previous state, you need the tombstones. So you trade document compactness for history.
The Authority Problem
The deeper mismatch isn’t technical, it’s architectural. Yjs is peer-to-peer by design. The invariant it maintains is that any two peers who exchange all their updates will converge to identical state, without needing to agree on anything beforehand. This is powerful and it’s the reason Yjs works without a central authority.
But most production applications need authority. A spreadsheet needs the server to evaluate formulas. A document editor might need to enforce that certain sections are read-only for certain users. A collaborative database tool needs to validate that a cell value matches a column type before accepting the write. None of these constraints fit the pure CRDT model.
You can layer authorization on top of Yjs by having the server filter or reject updates before broadcasting them. Some teams do this. The problem is that you’re now fighting the library’s assumptions. Yjs clients expect eventual convergence; if the server drops some updates based on business logic, clients can get into states the CRDT didn’t account for. You’re running an OT system inside a CRDT library, which is neither.
Compare this to ShareDB, which uses Operational Transformation and treats the server as the single source of truth. Every operation passes through the server, which applies it and broadcasts the canonical result. You lose the offline-first story, but you gain the ability to run arbitrary logic at commit time: validate, transform, reject, or audit every change. For applications where server authority matters, this trade is correct.
Automerge and the Alternative Path
The other major CRDT library in production use is Automerge, which took a different approach in its 2.0 rewrite. The team rewrote the core in Rust and exposed it via WebAssembly, achieving performance competitive with Yjs. Automerge is also schema-aware in a way Yjs isn’t — you can describe the shape of your document and get typed access to it.
Automerge also has a richer model for structured data. Where Yjs gives you Y.Map as a generic key-value CRDT, Automerge supports local-first schemas with counters, timestamps, and union types that carry semantic meaning through merges. If two clients concurrently set a counter, Automerge can add the increments rather than pick one arbitrarily.
But Automerge inherits the same fundamental constraints: no server authority, accumulating history, and GC that requires coordination. The choice between Yjs and Automerge is a choice between ecosystems and ergonomics, not a solution to the authority or accumulation problems.
Joseph Gentle’s Diamond Types is worth knowing about as a reference point. It’s a research-quality CRDT focused on pure performance, and it’s been used in benchmarks that show what the algorithmic ceiling looks like. It’s not production-ready as a library, but it demonstrates that raw throughput isn’t the constraint; the constraints are architectural.
What Moment Built Instead
Moment’s post is the second in a series — their first explored other myths about collaborative editing, and this one targets the specific library question. Reading between the lines of their reasoning, a custom server-authoritative system with selective conflict resolution is likely closer to what they ended up with. Spreadsheets are a case where the data model is simple (cells have values and formulas), the conflict semantics are well-defined (last writer wins per cell, formula re-evaluation is deterministic), and server authority is essential (you can’t let client-side CRDTs compute formulas; the server needs to own that).
This is a reasonable conclusion. The mistake isn’t using Yjs — it’s assuming Yjs solves the general problem of collaborative editing when it solves the specific problem of peer-to-peer convergence for unstructured content.
The Broader Lesson
CRDTs are a beautiful piece of distributed systems theory. The guarantee they provide — that any two replicas that exchange all messages will converge, regardless of order — is elegant and genuinely useful. Yjs implements this guarantee well, with a mature codebase, good performance characteristics, and a developer experience that makes the common case easy.
The problem is that the collaborative editing community, eager to escape the complexity of operational transformation and central servers, oversold CRDTs as a universal solution. Text editors are a near-perfect fit. Structured documents with business logic, permissions, and formula evaluation are not.
Before reaching for Yjs, it’s worth asking which of its properties you actually need. If you need offline-first with no server, peer-to-peer sync, and the document is primarily text or unstructured content, Yjs is a strong choice. If you need server-enforced constraints, bounded storage costs, and structured data with semantic conflict resolution, you’re likely building something that a CRDT library will fight against rather than help.
The Moment post is a useful corrective to a narrative that’s been running unchallenged for a few years. Yjs is good. It’s not universally good. That’s a distinction the collaborative editing ecosystem is still working out how to communicate clearly.