How it works
The shortest honest explanation
Flickerdocs is a real-time collaborative text editor with no central server, no database, and no AI in the loop. Two browsers connect directly, exchange edits over WebRTC, and merge them with a CRDT. This page explains how — at the level of detail an engineer would actually want, not at the level of a blog post.

The shape of the system
Three pieces, all in lib/:
identifier.js- Generates a unique, totally ordered identifier for every character ever inserted. Identifiers are fractional: between any two existing IDs, a new one can always be created that sorts strictly between them. So inserting "between A and B" never requires renumbering the document.
versionVector.js- Tracks which operations from which peers each replica has already applied. When a remote operation arrives, the version vector decides: skip (already applied) or apply.
crdt.js- The document — a sorted structure of characters, each tagged with its identifier. Inserts are placed by ID; deletes are looked up by ID. Two replicas applying the same operations in any order land in the same state.
That's it. The remaining files in lib/ are plumbing — broadcasting messages, wrapping characters, wiring the editor.
Why this converges
A CRDT works because three properties hold for every operation:
- Commutativity — order doesn't matter. Apply A then B, or B then A: same result.
- Associativity — grouping doesn't matter when batching ops.
- Idempotency — applying the same op twice is the same as applying it once.
Flickerdocs satisfies all three because every character has a globally unique identifier, every operation is keyed by that identifier, and the version vector skips duplicates. So peers can be offline, reconnect, replay history out of order, drop messages — and still converge. This property is sometimes called strong eventual consistency, and it is mathematical, not heuristic.
What a concurrent edit actually looks like
Suppose two peers are connected to the same document. Their cursors are colored pills with animal names — say a Lemur and a Hippopotamus. Both have the document "ab".
Internally, each character is tagged with a fractional identifier. Imagine a has ID [(0.3, Lemur)] and b has ID [(0.7, Lemur)] — both originally typed by Lemur.
Now both peers, at the exact same moment, with no network communication between them, decide to insert a different character between a and b:
- Lemur inserts
X. Their CRDT generates an identifier strictly between (0.3, Lemur) and (0.7, Lemur) — say[(0.5, Lemur)] — and broadcasts. - Hippopotamus inserts
Y. Their CRDT generates an identifier between the same two — say[(0.5, Hippopotamus)] — and broadcasts.
Two characters now exist with the "same" digit but different siteIds in the identifier. Identifier comparison breaks the tie deterministically using siteId. Both peers, regardless of which message arrives first, end up in the same state — say "aXYb" — because both peers run the same comparison. No coordinator. No serialization point. Just deterministic merging from the identifier scheme itself.
This is the moment the CRDT earns its name.
What it costs you
Fractional identifiers grow. Every "insert between" operation produces an identifier at least as long as the longer of its two neighbors. Under adversarial edit patterns — like a thousand peers all hammering at the same position — IDs can balloon. The original Logoot benchmarks made this a publication-worthy problem.
Flickerdocs uses a Logoot-family scheme without the LSEQ adaptive-allocation mitigation. For documents on the order of thousands of characters with single-digit peer counts, this is fine. For a real product at scale, it would not be.
OT vs. CRDT, briefly
Operational Transformation is the other approach to real-time collaborative editing. Google Docs uses it. The relevant difference for this project:
| OT | CRDT (this project) | |
|---|---|---|
| Convergence guarantee | Requires a central server to serialize ops | Mathematical — peers converge without coordination |
| Offline editing | Painful — divergent ops are tricky to transform | Trivial — apply locally, sync when reconnected |
| Implementation tax | Transformation functions are notoriously fragile | Identifier scheme is the main complexity |
| Network model | Centralized | Peer-to-peer works directly |
For an editor with no auth, no server, and no infrastructure budget, CRDT was the only sensible answer.
The papers behind this
- Shapiro et al., 2011 — "A comprehensive study of Convergent and Commutative Replicated Data Types." The foundational survey. Gives you the convergence math for CvRDT and CmRDT and the language to talk about it.
- Preguiça et al., 2009 — "Logoot: A Scalable Optimistic Replication Algorithm for Collaborative Editing." The original fractional-identifier paper for sequence CRDTs. The scheme this project uses is in this family.
- Nédelec et al., 2013 — "LSEQ: an Adaptive Structure for Sequences in Distributed Collaborative Editing." Solves Logoot's identifier-growth problem with a smarter allocation strategy. Worth reading even if you don't implement it.
What I'd build differently
For a real product I'd reach for Yjs. Its tree-of-structs CRDT scales better than Logoot, handles binary content, integrates with rich text via ProseMirror or TipTap, and ships with a mature ecosystem. It's the right answer for almost any user-facing product.
I built this project from scratch because I wanted to understand the primitives — not because I'd ship this exact code. Knowing the design space is more valuable than the code itself.
Read the source
All the CRDT logic — about 200 lines — lives in:
lib/crdt.js— document state, local + remote operation handlerslib/identifier.js— fractional identifier generationlib/versionVector.js— causality trackinglib/broadcast.js— WebRTC peer messaginglib/char.js— the character + identifier wrapper
Read in that order to follow the dataflow.