Real-Time Backend
Yjs in practice
Figma supports 100+ simultaneous editors in one file without a single conflict. How does that work without a central lock manager?
- Tldraw (whiteboard tool, 2M+ users) uses Y.Text and Y.Map to sync canvas objects between participants in real time with sub-50ms latency
- Heptabase (knowledge management, $5M ARR) builds collaborative cards on Yjs + y-redis on top of Redis Cluster, handling >1M operations/day with no server conflict resolver
- Loom (video messenger, acquired by Atlassian for $975M) uses Yjs for collaborative editing of subtitles and transcripts. Y.Text absorbs concurrent edits without merge conflicts
- Relm (3D collaboration) stores the full scene in Y.Array via y-webrtc, syncing object positions P2P with no dedicated server
Yjs shared types
Yjs builds CRDT collaboration on top of several shared data types, each of which resolves conflicts automatically with no server coordination. `Y.Doc` is the root container; every operation lands in it and then syncs between peers.
Core types
- `Y.Text` - a string with positional markers; used by Tldraw, Loom, and Heptabase for conflict-free collaborative text editing
- `Y.Map` - a dictionary with Last-Write-Wins semantics at the key level; good for metadata and configuration
- `Y.Array` - a list with CRDT-positioned elements; Relm (3D collaboration) stores its scene in one
- `Y.XmlFragment` / `Y.XmlElement` - a DOM tree with CRDT; used by ProseMirror-y-binding and TipTap
All types are obtained via `doc.getText(name)` / `doc.getMap(name)` / `doc.getArray(name)`. The name works as a namespace inside the document. One doc can hold an arbitrary number of named collections.
Two users insert text at position 3 in the same `Y.Text` at the same time. What happens after sync?
Yjs awareness
Awareness is an ephemeral layer on top of Y.Doc for state that shouldn't be persisted: cursors, online indicators, selections. Unlike CRDT operations, awareness state isn't accumulated in history. It lives only while the client is connected.
Tldraw and Heptabase use awareness to render other users' cursors in real time. Each client publishes its state, and the provider automatically broadcasts changes to all connected peers.
Awareness uses a heartbeat: if a client hasn't sent an update for 30 seconds (default), the provider drops its record from the shared state. This kills ghost cursors after unexpected disconnects.
What is the fundamental difference between Awareness and Y.Map for storing user cursors?
Yjs persistence
Yjs separates transport (who delivers changes) from storage (where they live). Persistence is the storage layer: IndexedDB on the client, PostgreSQL or Redis on the server. The key pattern is loading local state before connecting to the server so the document opens instantly even offline.
Yjs stores the full operation history (updates) in binary format. Over time the history grows: 10k operations in a `Y.Text` can take ~500KB. `Y.encodeStateAsUpdate(doc)` produces a minimal snapshot of the current state, useful for periodic compaction.
Why is `IndexeddbPersistence` initialized BEFORE `WebsocketProvider` in an offline-first app?
Yjs providers
A provider is a transport adapter that ships Yjs updates between peers. A single `Y.Doc` can attach multiple providers at once: WebSocket for online sync, IndexedDB for offline, for example. Updates flow through all attached providers automatically.
Main providers
- `y-websocket` - client-server provider; the y-websocket server holds up to 10k concurrent rooms on a single Node.js process (used by Loom and Tldraw in dev)
- `y-webrtc` - P2P via WebRTC DataChannel with a signaling server; Relm uses it for 3D collaboration without a central server
- `y-redis` - server provider on Redis Streams; Heptabase runs it on top of Redis Cluster for horizontal scaling
- `y-leveldb` - server persistence via LevelDB; suitable for single-node deploys without Redis
Yjs uses `stateVector` for deduplication: every update carries `(clientID, clock)`, and the provider does not apply an update already present in `doc.store`. That makes duplicating updates across multiple providers safe - idempotency is built into the protocol.
Pick one provider and use only it, otherwise data will get duplicated
Multiple providers for one Y.Doc is the recommended production pattern: WebSocket for the main channel, IndexedDB for offline, WebRTC as a P2P fallback
Yjs is idempotent by design: the state vector guarantees each update applies exactly once regardless of how many transport channels deliver it. Heptabase, Loom, and Tldraw use 2-3 providers at once for resilience.
A production app uses `y-websocket` and `y-webrtc` for the same Y.Doc. The client receives the same update through both channels. What happens?
Key takeaways
- Y.Doc is the root container of a CRDT document; Y.Text, Y.Map, Y.Array are typed shared structures with automatic conflict resolution
- Awareness is an ephemeral layer for cursors and online state: not persisted, auto-removed on client disconnect via heartbeat (30s)
- Persistence is decoupled from transport: IndexedDB on the client for offline-first, y-redis/y-leveldb on the server for operation history
- Multiple providers on one doc is a production norm: the state vector guarantees idempotency, duplicate updates are dropped automatically
Related topics
Yjs implements the theory covered in related lessons:
- CRDT: conflict-free algorithms — Yjs is a concrete CRDT implementation (the YATA algorithm for Y.Text). Theory helps predict behavior on concurrent edits
- WebSocket and real-time — The y-websocket provider uses WebSocket as transport. Understanding ping/pong and reconnect logic explains provider behavior on flaky networks
- Eventual Consistency — Yjs guarantees strong eventual consistency: all replicas converge to the same state after receiving all updates, with no coordination
Вопросы для размышления
- If Y.Text stores all operation history forever, how should an app manage document size growth over a long-running collaboration?
- When is y-webrtc (P2P) preferable to y-websocket (client-server), and where can that architectural choice cause problems?
- Awareness drops state 30 seconds after the last heartbeat. How does that affect UX on flaky mobile networks, and how can it be compensated?
Связанные уроки
- rt-49 — Yjs implements the RGA algorithm covered in the text CRDT lesson
- rt-51 — Automerge is the alternative production CRDT framework, with a different trade-off between throughput and history fidelity
- ds-10-crdts