blog.dopana

Back

Building real-time collaborative applications like Google Docs, Figma, or Notion used to be one of the hardest engineering challenges in web development. Handling network latency, disconnections, and resolving conflicts when multiple users type at the same spot required complex logic.

Today, libraries like Yjs have made this task significantly easier. Let’s dive into what Yjs is, how it works under the hood, and why it is the go-to tool for modern collaborative applications.

What is Yjs? (ELI5)#

Imagine you and a friend are editing a story in a notebook at the same time.

In traditional collaborative systems (like Google Docs, which uses Operational Transformation / OT), you need a teacher (a central server) standing between you. When you write a word, you must show it to the teacher first. The teacher decides who wrote what first, adjusts the positions of your text, and tells both of you the final result. If the teacher goes away (the server goes offline), you can no longer write.

Yjs uses CRDTs (Conflict-free Replicated Data Types), which work without a teacher:

  • Every letter you write gets a unique invisible sticker (ID) and notes its neighbor (“this letter goes right after letter X”).
  • You and your friend can write whatever you want, even if you are in different rooms (offline).
  • When you meet later and swap notes, you just look at the stickers and neighbors to merge your pages. Because the rules for merging are mathematical and deterministic, both of your notebooks will end up looking exactly the same, with zero conflicts!

Core Architecture & Workflow#

Yjs acts as a network-agnostic data model. It maintains a local document state (Y.Doc) and converts updates into binary packets that can be sent over any protocol (WebSockets, WebRTC, or even email).

sequenceDiagram
    participant A as Client A
    participant M as Network WebSocket
    participant B as Client B
    Note over A: ydoc.getMap().set('x', 1)
    A->>M: encodeStateAsUpdate (binary)
    Note over B: ydoc.getMap().set('y', 2)
    B->>M: encodeStateAsUpdate (binary)
    M->>B: Deliver Client A update
    Note over B: Y.applyUpdate() - Merged state
    M->>A: Deliver Client B update
    Note over A: Y.applyUpdate() - Merged state

1. Shared Types#

Yjs provides shared data types that behave like normal JavaScript structures but sync automatically:

  • Y.Text: Used for collaborative text editors.
  • Y.Array: A collaborative list.
  • Y.Map: A collaborative key-value store.
  • Y.XmlFragment: Used for rich-text structural trees.

2. Connection Providers#

Yjs is completely network-agnostic. You plug in a Provider to handle communication:

  • y-websocket: Standard server-client synchronization.
  • y-webrtc: Peer-to-peer syncing in the browser without a backend.
  • y-p2p / y-matrix: Alternative decentralized protocols.

3. Database Providers (Persistence)#

To prevent users from losing their edits when closing the tab, persistence providers store document updates:

  • y-indexeddb: Local browser storage for offline editing.
  • y-leveldb / y-redis: Backend database adapters for persistent storage on servers.

How Yjs Works Under the Hood (YATA)#

Yjs is based on the YATA (Yet Another Transformation Approach) algorithm. It ensures convergence and intention preservation using a few clever tricks:

  • Immutable Unique IDs: Every inserted item (like a character) has a unique Client ID and a local transaction counter.
  • Origin Anchors: Every character remembers its left neighbor (origin) and right neighbor (originRight) at the moment of insertion. Even if the surrounding text changes, the character remains anchored to its context.
  • Run-Length Encoding (RLE): To prevent huge memory consumption (since every character needs metadata), Yjs groups consecutive inserts from the same client into a single “struct”. This makes Yjs extremely fast and memory-efficient.

Quick Code Example#

Here is how you can use Yjs to sync maps and arrays:

References#