Two users edit a rectangle in your design tool while offline. Alice moves it to
position `[100, 200]`. Bob changes its color to `red`. When they reconnect, what
should happen? This is easy to answer—show the rectangle at `[100, 200]` in
`red`. Both changes survive.

Now the hard questions:

- What if they _both moved it_ to different positions?
- What if Alice _deleted_ the rectangle while Bob moved it?
- What if they're _both typing_ at the same position in a text field?

**Your answers to these questions determine your entire sync architecture.**
Choose wrong, and you're looking at 3-6 months of refactoring when you realize
users are losing data.

Every modern app is adding “realtime collaboration” to their feature list, and
you've probably heard CRDT thrown around like it's a silver bullet. The reality
is far more nuanced: Figma’s collaborative canvas works fundamentally
differently than Google Docs’ text editing, which works differently than
Linear's issue tracking. They’re all “realtime” and “collaborative”, but the
underlying sync mechanisms solve different problems.

As someone who’s built realtime systems at scale—from
[social media platforms with offline-first sync](https://bytevagabond.com/post/building-a-social-media-startup/)
to [event-driven microservice architectures](https://heichling.xyz/)—I've seen
how critical this architectural decision becomes. This guide synthesizes what
production systems actually do and why.

## Solving concurrent editing

Every realtime collaborative system must be able to handle multiple users
editing the same data simultaneously, with network delays and offline periods.
Changes can arrive in any order, and everyone needs to end up with the same
result.

Let’s think back to Alice and Bob’s rectangle, and some conflicts they’ll face
in a multiplayer app:

- Alice and Bob change different properties? Easy. Both changes survive.
- They edit the same property differently? Tricky. A conflict resolution
  strategy is needed.
- Alice deletes, Bob modifies? Tricky. An intention conflict needs to be
  resolved.
- Both typing in the same text position? Difficult. This is where most naive
  implementations break.

There are two solution families that handle these scenarios differently.

### Operational Transformation (OT)

Operational Transformation (OT) transforms operations based on what others did
concurrently. Requires a central server to order operations, and is used by
Google Docs, Microsoft Office Online. Complex to implement correctly, but gives
predictable results for text editing.

### Conflict-free Replicated Data Types (CRDTs)

Conflict-free Replicated Data Types (CRDTs) operations are mathematically
commutative—apply them in any order, always converge to the same state. Works
peer-to-peer or client-server. True CRDTs underpin frameworks like Yjs and
Automerge, powering many collaborative text editors and document tools.

_CRDT-inspired_ architectures power
[Figma](https://www.figma.com/blog/how-figmas-multiplayer-technology-works/),
[Linear](https://linear.app/blog/scaling-the-linear-sync-engine), and products
built with Liveblocks like
[Vercel](https://liveblocks.io/blog/how-vercel-used-live-reactions-to-improve-engagement-on-their-vercel-ship-livestream),
[Hashnode](https://liveblocks.io/blog/how-hashnode-added-collaboration-to-their-text-editor-to-sell-to-larger-organizations),
and
[Resend](https://liveblocks.io/blog/how-resend-transformed-email-collaboration-with-real-time-multiplayer-editing).

### What’s the difference?

Both enable “realtime collaboration”, but they’re designed for fundamentally
different problems. Figma’s rectangle positioning needs different conflict
resolution than Google Docs’ text editing. The key insight is that it’s not
about _OT vs CRDT_—it’s about _property-level vs character-level_ conflict
resolution.

## Conflict resolution methods

**Property-level conflict resolution** treats each attribute of an object as an
independent unit. When two users edit different properties of the same object,
both changes succeed. When they edit the same property, Last Write Wins (LWW)
based on a logical clock.

**Character-level conflict resolution** treats text as a sequence of characters
with positional metadata. When two users edit different parts of the same text,
both edits are preserved in the final result through sophisticated merging
algorithms.

<div className="my-8 md:my-10">
  <Figure>
    <Image
      src="/images/blog/understanding-sync-engines-how-figma-linear-and-google-docs-work/crdt-comparison.jpg"
      alt="A custom code block with syntax highlighting, inside an AI chat"
      width={672}
      height={526}
      quality={90}
    />
  </Figure>
  <div className="markdown mx-auto max-w-sm text-center text-xs! text-marketing-subtler *:mt-2.5! lg:max-w-md lg:text-sm!">
    Property-level uses last-write-wins per property, where character-level
    merges based on position.
  </div>
</div>

### Concrete Examples

Two users are editing a task item while offline:

```tsx title="Initial state"
const task = {
  title: "Buy apples",
  done: false,
};
```

```tsx title="User A (offline): Updates title"
const changeA = {
  attribute: "title",
  // +++
  value: "Buy apples and oranges",
  // +++
  timestamp: "2024-01-15T10:30:00Z",
};
```

```tsx title="User B (offline): Marks as done"
const changeB = {
  attribute: "done",
  // +++
  value: true,
  // +++
  timestamp: "2024-01-15T10:30:05Z",
};
```

```tsx title="After sync with property-level resolution"
// Both changes survive because they modified different properties
const task = {
  // +++
  title: "Buy apples and oranges", // User A's change
  done: true, // User B's change
  // +++
};
```

This is **property-level conflict resolution**, used by Figma and Liveblocks
Storage. Each property is independent, and changes to different properties never
conflict.

---

Now consider the same scenario with text editing:

```text title="Initial state"
Buy apples
```

```md title="User A (offline): Adds to the end"
Buy apples `and oranges`
```

```md title="User B (offline): Adds to the end"
Buy apples `and bananas`
```

```md title="After sync with character-level resolution, both edits are merged"
Buy apples `and oranges and bananas`
```

This is **character-level conflict resolution**, used by Google Docs and Yjs.
The CRDT tracks the position and context of each character insertion, allowing
both edits to coexist in the final document. This difference determines your
entire sync architecture.

The theory is interesting, but what do real companies with millions of users
actually do? Let’s examine three battle-tested systems.

## Which systems are used in production?

### Figma: Property-level Last Write Wins

[Figma’s multiplayer system](https://www.figma.com/blog/how-figmas-multiplayer-technology-works/)
is one of the most well-documented collaborative architectures. How it works:

```tsx title="Figma’s document structure (simplified)"
type FigmaDocument = Map<ObjectID, Map<Property, Value>>;

// Example: A rectangle object
const rectangle = {
  id: "rect_123",
  properties: {
    x: 100,
    y: 200,
    width: 50,
    height: 30,
    fill: "#FF0000",
  },
};
```

Figma’s servers track the latest value for each property on each object, as
described on
[Figma’s engineering blog](https://www.figma.com/blog/how-figmas-multiplayer-technology-works/).
When two clients change different properties (one moves the rectangle, another
changes its color), both changes succeed. When two clients change the same
property, the last change received by the server wins.

<BlogPostQuote
  quote="Figma's multiplayer servers keep track of the latest value that any client has
sent for a given property on a given object. This means that two clients
changing unrelated properties on the same object won't conflict."
  author={{
    name: "Evan Wallace",
    title: "Co-founder, Figma",
    avatarUrl: "/images/people/evan-wallace.jpg",
    companyAvatarUrl: "/images/companies/avatars/figma.png",
    companyName: "Figma",
  }}
/>

This is explicitly _not_ a true CRDT because it relies on a central server for
ordering, but it’s inspired by CRDT concepts—specifically the
[Last-Writer-Wins Register pattern](<https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type#LWW-Element-Set_(Last-Write-Wins-Element-Set)>).
Liveblocks Storage uses this same proven architecture with centralized CRDT-like
structures. I’ve implemented similar patterns in production social platforms—the
simplicity is what makes it reliable at scale. Figma chose this approach
because:

<Table scrollable>
  <TableHead>
    <TableRow>
      <TableCell header>Reason</TableCell>
      <TableCell header>Impact</TableCell>
    </TableRow>
  </TableHead>
  <TableBody>
    <TableRow>
      <TableCell subtle={false}>
        Design tools work with discrete objects
      </TableCell>
      <TableCell>
        Each shape, layer, or component is an independent entity
      </TableCell>
    </TableRow>
    <TableRow>
      <TableCell subtle={false}>Properties update independently</TableCell>
      <TableCell>Color changes don’t conflict with position changes</TableCell>
    </TableRow>
    <TableRow>
      <TableCell subtle={false}>
        Users rarely edit same property simultaneously
      </TableCell>
      <TableCell>Natural work patterns reduce conflicts</TableCell>
    </TableRow>
    <TableRow>
      <TableCell subtle={false}>Last-write-wins is intuitive</TableCell>
      <TableCell>
        When conflicts occur, newest wins matches user expectations
      </TableCell>
    </TableRow>
    <TableRow>
      <TableCell subtle={false}>
        Significantly simpler than full CRDTs
      </TableCell>
      <TableCell>Faster performance, easier to debug</TableCell>
    </TableRow>
  </TableBody>
</Table>

Figma’s approach doesn’t work for text editing. If the text “B” becomes “AB” on
one client and “BC” on another, the result is either “AB” or “BC”—never “ABC”.
This is fine for design tools but unacceptable for collaborative text editors.

### Linear: Mostly Last Write Wins with selective CRDTs

Linear’s sync engine is designed for a more varied data model than Figma (as
[documented in their blog](https://linear.app/blog/scaling-the-linear-sync-engine)
and
[reverse-engineered here](https://github.com/wzhudev/reverse-linear-sync-engine))
and uses a similar property-level approach for most data:

```tsx title="Linear's change structure (reverse-engineered)"
type LinearChange = {
  uuid: string;
  attribute: "title" | "status" | "priority" | "assignee";
  value: any;
  syncId: number; // Server-assigned monotonic ID
};
```

Linear tracks changes as discrete events, each modifying a single attribute. The
server assigns each change a monotonically increasing sync ID, and conflicts
resolve to the highest ID (effectively last-write-wins, but server-ordered).

<Table scrollable>
  <TableHead>
    <TableRow>
      <TableCell subtle={false} header>
        Linear Design Choice
      </TableCell>
      <TableCell header>Rationale</TableCell>
    </TableRow>
  </TableHead>
  <TableBody>
    <TableRow>
      <TableCell subtle={false}>Event-based architecture</TableCell>
      <TableCell>Every change is a separate event with metadata</TableCell>
    </TableRow>
    <TableRow>
      <TableCell subtle={false}>Property-level granularity</TableCell>
      <TableCell>Status changes don't conflict with title edits</TableCell>
    </TableRow>
    <TableRow>
      <TableCell subtle={false}>Server-ordered sync IDs</TableCell>
      <TableCell>
        Reliable ordering through centralized transaction processing
      </TableCell>
    </TableRow>
    <TableRow>
      <TableCell subtle={false}>Last-Write-Wins default</TableCell>
      <TableCell>Conflicts are rare; simple resolution works</TableCell>
    </TableRow>
    <TableRow>
      <TableCell subtle={false}>CRDTs only for rich text</TableCell>
      <TableCell>Added later for issue descriptions only</TableCell>
    </TableRow>
  </TableBody>
</Table>

According to
[their engineering team's discussions](https://linear.app/now/scaling-the-linear-sync-engine),
conflicts are actually quite rare in their domain—most of the time users are
working on different issues or different properties of the same issue. They only
recently added CRDTs for one specific use case; rich-text editing in issue
descriptions.

### Google Docs & Yjs: Character-level OT & CRDTs

Both Figma and Linear use property-level approaches because they work with
discrete objects where conflicts are rare. Text editing is fundamentally
different—users constantly insert characters at nearby or identical positions.
This is where character-level resolution becomes essential.

Google Docs and Yjs both handle character-level text editing, but use different
approaches. Google Docs uses Operational Transformation (server-based), while
Yjs uses CRDTs (peer-to-peer). Both recognize that text editing has
fundamentally different conflict patterns than object properties.

#### How Yjs is structured

Here’s how Yjs structures text for CRDT-based collaboration:

```tsx title="Yjs document structure (simplified)"
type YjsText = Array<{
  char: string;
  id: {
    client: string;
    clock: number;
  };
  left: ID | null; // Previous character
  right: ID | null; // Next character
}>;
```

Every character gets a unique identifier tracking its position and creation
context. When two users type at the same spot simultaneously, Yjs uses
[a deterministic algorithm](https://doi.org/10.1145/2957276.2957310) to decide
which text comes first based on these identifiers:

```md title="User A types"
`Hello world`
```

```md title="User B types"
`Hi there`
```

```md title="Result without Yjs (Last Write Wins)"
`Hello world` OR `Hi there`
```

```md title="Result with Yjs"
`Hi Hello world` OR `HelloHi there`
```

This is fundamentally different from property-level sync. Yjs operates on
ordered sequences—treating text as individual characters with positional
relationships. You can’t just “swap in Yjs” for a Figma-like app because it’s
solving a different problem: preserving the order and intent of concurrent
insertions in sequences, not resolving conflicts between discrete object
properties.

You’ve seen what Figma, Linear, and Google Docs do, now let's determine which
approach fits _your_ application.

## Which approach do you need?

### When to use property-level resolution (Liveblocks Storage)

Property-level resolution is fully supported by
[Liveblocks Storage](/docs/ready-made-features/multiplayer-editing/sync-engine/liveblocks-storage).

<Table scrollable>
  <TableHead>
    <TableRow>
      <TableCell subtle={false} header>
        Use Case Category
      </TableCell>
      <TableCell header>Examples</TableCell>
      <TableCell header>Why property-level works</TableCell>
    </TableRow>
  </TableHead>
  <TableBody>
    <TableRow>
      <TableCell subtle={false}>Visual/spatial editors</TableCell>
      <TableCell>Figma, Miro, Whimsical, Lucidchart</TableCell>
      <TableCell>
        Objects have discrete properties (position, size, color)
      </TableCell>
    </TableRow>
    <TableRow>
      <TableCell subtle={false}>Diagramming tools</TableCell>
      <TableCell>Draw.io, flowchart builders</TableCell>
      <TableCell>Nodes and edges are independent objects</TableCell>
    </TableRow>
    <TableRow>
      <TableCell subtle={false}>Structured data apps</TableCell>
      <TableCell>Kanban boards, form builders, spreadsheet cells</TableCell>
      <TableCell>
        Each item has distinct properties that rarely conflict
      </TableCell>
    </TableRow>
    <TableRow>
      <TableCell subtle={false}>Configuration tools</TableCell>
      <TableCell>Dashboard builders, workflow designers</TableCell>
      <TableCell>
        Changes typically affect different objects/properties
      </TableCell>
    </TableRow>
  </TableBody>
</Table>

**Conflict characteristics that favor property-level:**

- Users typically work on different objects or different properties.
- When same-property conflicts occur, last write wins is acceptable.
- You need to track “who changed what” at the property level.
- Objects have clear identity and structured properties.

#### Code example

[`LiveObject`](/docs/api-reference/liveblocks-client#LiveObject) is a CRDT-like
data structure that allows these changes to be merged automatically.

```tsx title="Liveblocks storage example"
import { LiveObject } from "@liveblocks/client";

// Create a shape, each property is independently mutable
const rectangle = new LiveObject({
  type: "rectangle",
  x: 0,
  y: 200,
  fill: "blue",
});

// User A moves the rectangle
rectangle.set("x", 50);

// User B changes the color (simultaneously)
rectangle.set("fill", "red");

// Both changes survive, no conflicts
// {
//   type: "rectangle",
//   x: 50,
//   y: 200,
//   fill: "red",
// }
rectangle.toImmutable();
```

It’s also possible to use
[LiveMap](/docs/api-reference/liveblocks-client#LiveMap) and
[LiveList](/docs/api-reference/liveblocks-client#LiveList) to manage complex
data structures, nesting them inside each other.

### When to Use Character-Level Resolution (Liveblocks Yjs)

Character-level resolution is fully supported by
[Liveblocks Yjs](/docs/ready-made-features/multiplayer-editing/sync-engine/liveblocks-yjs)
and our
[text editor integrations](/docs/ready-made-features/multiplayer-editing/text-editor).

<Table scrollable>
  <TableHead>
    <TableRow>
      <TableCell subtle={false} header>
        Use Case Category
      </TableCell>
      <TableCell header>Examples</TableCell>
      <TableCell header>Why Character-level Is required</TableCell>
    </TableRow>
  </TableHead>
  <TableBody>
    <TableRow>
      <TableCell subtle={false}>Text/rich-text editors</TableCell>
      <TableCell>Google Docs, Notion, Medium</TableCell>
      <TableCell>Multiple users typing in same paragraph</TableCell>
    </TableRow>
    <TableRow>
      <TableCell subtle={false}>Code editors</TableCell>
      <TableCell>VSCode Live Share, Replit</TableCell>
      <TableCell>Character-level precision for code</TableCell>
    </TableRow>
    <TableRow>
      <TableCell subtle={false}>Markdown editors</TableCell>
      <TableCell>HackMD, CodiMD</TableCell>
      <TableCell>Text content is primary data</TableCell>
    </TableRow>
    <TableRow>
      <TableCell subtle={false}>Messaging/comments</TableCell>
      <TableCell>Slack-like apps with rich text</TableCell>
      <TableCell>Insertion order and position matter</TableCell>
    </TableRow>
  </TableBody>
</Table>

**Conflict characteristics that require character-level:**

- Multiple users editing the same text simultaneously.
- Insertion position and order are critical.
- Character-level granularity is required.
- Delete operations need to be preserved.
- Undo/redo must work correctly in collaborative context.

#### Code example

[Liveblocks Yjs](/docs/ready-made-features/multiplayer-editing/sync-engine/liveblocks-yjs)
is a CRDT-like data structure that allows these changes to be merged
automatically.

```tsx title="Liveblocks Yjs example"
const yText = yDoc.getText("content");

// User A types at position 0
yText.insert(0, "Hello");

// User B types at position 0 simultaneously
yText.insert(0, "Hi ");

// Both insertions preserved, merged deterministically
// "Hi Hello"
yText.toString();
```

### The Decision Tree

Use this tree to determine which approach to use for your application.

<Figure>
  <Image
    src="/images/blog/understanding-sync-engines-how-figma-linear-and-google-docs-work/decision-tree.jpg"
    alt="A custom code block with syntax highlighting, inside an AI chat"
    width={672}
    height={611}
    quality={90}
  />
</Figure>

## What actually matters in production

After studying production systems:

<Table scrollable>
  <TableHead>
    <TableRow>
      <TableCell subtle={false} header>
        Technique
      </TableCell>
      <TableCell header>Why It Matters</TableCell>
      <TableCell header>When to Use</TableCell>
    </TableRow>
  </TableHead>
  <TableBody>
    <TableRow>
      <TableCell subtle={false}>
        <strong>Property-level LWW</strong>
      </TableCell>
      <TableCell>Matches user mental models for object editing</TableCell>
      <TableCell>90% of design tools, structured data apps</TableCell>
    </TableRow>
    <TableRow>
      <TableCell subtle={false}>
        <strong>Character-level CRDTs</strong>
      </TableCell>
      <TableCell>Required for concurrent text editing</TableCell>
      <TableCell>Any collaborative text/code editor</TableCell>
    </TableRow>
    <TableRow>
      <TableCell subtle={false}>
        <strong>Logical clocks</strong>
      </TableCell>
      <TableCell>
        System timestamps are unreliable in distributed systems
      </TableCell>
      <TableCell>
        Distributed systems without central ordering authority
      </TableCell>
    </TableRow>
    <TableRow>
      <TableCell subtle={false}>
        <strong>Optimistic updates</strong>
      </TableCell>
      <TableCell>Users expect instant feedback</TableCell>
      <TableCell>Every realtime collaborative app</TableCell>
    </TableRow>
    <TableRow>
      <TableCell subtle={false}>
        <strong>Presence/awareness</strong>
      </TableCell>
      <TableCell>Seeing others' cursors is table stakes</TableCell>
      <TableCell>Apps with multiple simultaneous editors</TableCell>
    </TableRow>
  </TableBody>
</Table>

**What doesn't matter as much:**

- **OT vs CRDT debate:**
  [Yjs and CRDTs won](https://josephg.com/blog/crdts-are-the-future/). Use
  CRDTs.
- **P2P vs client-server:** Client-server is simpler and works fine for most use
  cases.
- **Custom CRDT implementations:** The [research is solved](https://crdt.tech/).
  Use existing solutions.

At this point you might be thinking: “This seems straightforward—can’t I just
build it myself?” Here’s what that actually entails.

## Why not roll your own?

The decision framework seems straightforward, but you might be thinking: “Can’t
I just build this myself?”. It’s harder than it looks, which is why platforms
exist to solve this. When I talk to clients who built their own sync engine,
they consistently underestimated the following components.

### Logical clocks that actually work

You can’t just use `Date.now()` for timestamps. System clocks drift, users
change their time zones, and servers can have clock skew. One approach is Hybrid
Logical Clocks.

```tsx
type HybridLogicalClock = {
  wallTime: number; // Physical time component
  counter: number; // Logical counter for same wallTime
  nodeId: string; // Unique node identifier for ties
};

// When receiving a remote timestamp
function receiveTimestamp(localClock: HLC, remoteClock: HLC): HLC {
  const maxWallTime = Math.max(localClock.wallTime, remoteClock.wallTime);
  const physicalTime = Date.now();

  if (maxWallTime >= physicalTime) {
    // Logical time is ahead of physical - increment counter
    return {
      wallTime: maxWallTime,
      counter: localClock.counter + 1,
      nodeId: localClock.nodeId,
    };
  } else {
    // Physical time caught up - reset counter
    return {
      wallTime: physicalTime,
      counter: 0,
      nodeId: localClock.nodeId,
    };
  }
}
```

Note that systems with centralized servers (like Linear and Figma) can sidestep
this complexity with server-assigned ordering, but truly distributed systems
need HLCs or similar mechanisms. The
[original HLC paper](https://cse.buffalo.edu/tech-reports/2014-04.pdf) details
why this matters for distributed systems.

### Efficient storage and indexing

You need a database schema that handles millions of events efficiently. Here’s
the difference:

```sql
-- Naive approach that doesn't scale
CREATE TABLE changes (
  id BIGSERIAL PRIMARY KEY,
  document_id UUID,
  attribute TEXT,
  value JSONB,
  timestamp BIGINT
);

-- Production approach
CREATE TABLE changes (
  id BIGSERIAL PRIMARY KEY,
  document_id UUID,
  client_id TEXT,
  client_sequence INTEGER,  -- Client's local counter
  attribute TEXT,
  value JSONB,
  server_timestamp BIGINT,  -- Server-assigned ordering
  deleted BOOLEAN,

  UNIQUE(document_id, client_id, client_sequence)
);

CREATE INDEX idx_changes_sync
  ON changes(document_id, server_timestamp)
  WHERE NOT deleted;
```

The production schema tracks both client-side causality (`client_id` +
`client_sequence`) and server-side ordering (`server_timestamp`). When conflicts
occur, server timestamp wins. The `UNIQUE` constraint prevents duplicate
operations from the same client.

This approach, used by Linear and Figma, is simpler than full vector clocks but
reliable because the server provides total ordering.

### What you’re actually building

Before you know it, an array of tasks will be on your hands.

<Table scrollable>
  <TableHead>
    <TableRow>
      <TableCell subtle={false} header>
        Component
      </TableCell>
      <TableCell header>Complexity</TableCell>
      <TableCell header>Why it’s hard</TableCell>
    </TableRow>
  </TableHead>
  <TableBody>
    <TableRow>
      <TableCell subtle={false}>
        <strong>Logical clocks</strong>
      </TableCell>
      <TableCell>High</TableCell>
      <TableCell>
        HLCs require careful implementation; bugs cause silent data corruption
      </TableCell>
    </TableRow>
    <TableRow>
      <TableCell subtle={false}>
        <strong>Database Schema</strong>
      </TableCell>
      <TableCell>Medium</TableCell>
      <TableCell>
        Must handle millions of events, efficient queries, garbage collection
      </TableCell>
    </TableRow>
    <TableRow>
      <TableCell subtle={false}>
        <strong>Offline Sync</strong>
      </TableCell>
      <TableCell>Very High</TableCell>
      <TableCell>
        Queuing, reconnection, merge conflicts, UI updates during merge
      </TableCell>
    </TableRow>
    <TableRow>
      <TableCell subtle={false}>
        <strong>Presence System</strong>
      </TableCell>
      <TableCell>Medium</TableCell>
      <TableCell>
        Separate broadcast system for cursors, selections, typing indicators
      </TableCell>
    </TableRow>
    <TableRow>
      <TableCell subtle={false}>
        <strong>Permission Control</strong>
      </TableCell>
      <TableCell>High</TableCell>
      <TableCell>
        Row-level security, encryption, audit logs, graceful access revocation
      </TableCell>
    </TableRow>
    <TableRow>
      <TableCell subtle={false}>
        <strong>WebSocket Infrastructure</strong>
      </TableCell>
      <TableCell>Very High</TableCell>
      <TableCell>
        Connection pooling, load balancing, heartbeats, mobile sleep/wake
        handling
      </TableCell>
    </TableRow>
  </TableBody>
</Table>

6-12 months of engineering time from experienced distributed systems engineers,
plus ongoing maintenance, monitoring, and debugging infrastructure.

## Practical Implementation

The theory is clear. Now let’s build it. You have two options:

**Option 1: Build it yourself** (6-12 months, 2-3 engineers)

- Implement logical clocks, conflict resolution, WebSocket infrastructure.
- See [Why not roll your own?](#why-not-roll-your-own) for the full task list.

**Option 2: Use Liveblocks** (hours to days)

- Handles sync infrastructure, storage, and conflict resolution.
- Two products matching the patterns above.

## Implementing these patterns with Liveblocks

This complexity is exactly why Liveblocks exists—to handle the difficult sync
infrastructure so you focus on your application. Liveblocks is the only hosted
solution that offers both sync types, providing:

- **Liveblocks Storage** for property-level sync (the Figma/Linear pattern)
- **Liveblocks Yjs** for character-level sync (the Google Docs pattern)

Let’s see how to implement each:

### Liveblocks Storage: Property-level sync

**Architecture pattern:**

```tsx
import { createClient } from "@liveblocks/client";
import { LiveObject, LiveList, LiveMap } from "@liveblocks/client";

const client = createClient({
  publicApiKey: "pk_prod_...",
});

// Enter a room with typed storage
const { room } = client.enterRoom("design-doc-123", {
  initialStorage: {
    // Map of shape ID to shape object
    shapes: new LiveMap<string, LiveObject<Shape>>(),
    // Ordered list for layers
    layers: new LiveList<string>(),
  },
});

// Subscribe to storage changes
room.subscribe(room.getStorage().root, () => {
  // React to any storage change
  const shapes = room.getStorage().root.get("shapes");
  renderCanvas(shapes);
});

// Make changes that sync automatically
const shapes = room.getStorage().root.get("shapes");
const shape = new LiveObject({
  type: "rectangle",
  x: 100,
  y: 100,
  width: 200,
  height: 100,
  fill: "#FF0000",
});

shapes.set("rect-1", shape);

// Later: update individual properties
shape.set("x", 150); // Only x syncs, not the whole object
```

**Key capabilities:**

- [LiveObject, LiveMap, LiveList](https://liveblocks.io/docs/api-reference/liveblocks-client#LiveObject)
  CRDT-like data structures.
- Automatic conflict resolution per property.
- Optimistic updates with automatic sync.
- Connection handling with offline detection.
- Built-in
  [presence](https://liveblocks.io/docs/api-reference/liveblocks-client#Presence)
  (cursors, selections).
- Packages for React, Zustand, Redux.

### Liveblocks Yjs: Character-level sync

**Architecture pattern:**

```tsx
import { createClient } from "@liveblocks/client";
import LiveblocksProvider from "@liveblocks/yjs";
import * as Y from "yjs";
import { TiptapEditor } from "@tiptap/core";
import Collaboration from "@tiptap/extension-collaboration";

const client = createClient({
  publicApiKey: "pk_prod_...",
});

const room = client.enter("document-123", {
  initialPresence: { cursor: null },
});

// Create Yjs document
const yDoc = new Y.Doc();
const yText = yDoc.getText("content");

// Connect to Liveblocks
const provider = new LiveblocksProvider(room, yDoc);

// Bind to editor
const editor = new TiptapEditor({
  extensions: [
    Collaboration.configure({
      document: yDoc,
    }),
  ],
});

// All edits now sync through Yjs CRDT
// Multiple users can type simultaneously
```

**Key capabilities:**

- Full [Yjs CRDT implementation](https://docs.yjs.dev/).
- Managed WebSocket infrastructure.
- Persistent storage for Yjs documents.
- Easy integrations for popular text editors:
  [Lexical](/docs/ready-made-features/multiplayer-editing/text-editor/lexical),
  [Tiptap](/docs/ready-made-features/multiplayer-editing/text-editor/tiptap),
  [BlockNote](/docs/ready-made-features/multiplayer-editing/text-editor/blocknote).
- [REST API](https://liveblocks.io/docs/api-reference/rest-api-endpoints) and
  Node.js packages for server-side modifications.

### Using both together

Many applications need both:

```tsx
// Notion-style app: Storage for structure, Yjs for content

// Room setup with both
const room = client.enterRoom("page-123", {
  initialStorage: {
    // Page metadata and structure - Storage
    metadata: new LiveObject({
      title: "Project Notes",
      emoji: "📝",
      coverImage: null,
    }),
    // Block structure - Storage
    blocks: new LiveList<LiveObject<Block>>(),
  },
});

// Each text block gets its own Yjs document
const blockDoc = new Y.Doc();
const blockText = blockDoc.getText("content");
const blockProvider = new LiveblocksProvider(room, blockDoc);

// Result:
// - Block positions/order synced via Storage (property-level)
// - Text content within blocks synced via Yjs (character-level)
```

## Conclusion

The collaborative sync landscape has matured significantly. The fundamental
patterns, property-level LWW for objects, character-level CRDTs for text, are
well understood and battle-tested in production
([Figma](https://www.figma.com/blog/how-figmas-multiplayer-technology-works/),
[Linear](https://linear.app/blog/scaling-the-linear-sync-engine),
[Yjs](https://docs.yjs.dev/)).

These are different problems requiring different solutions. Figma doesn't use
Yjs because property-level sync is the right model for design tools. Google Docs
doesn't use property-level sync because character-level CRDTs are required for
text editing.

**Choose based on your primary data model:**

<Table scrollable>
  <TableHead>
    <TableRow>
      <TableCell subtle={false} header>
        If you're building...
      </TableCell>
      <TableCell header>Use...</TableCell>
    </TableRow>
  </TableHead>
  <TableBody>
    <TableRow>
      <TableCell subtle={false}>
        Objects with properties (design tools, kanban boards)
      </TableCell>
      <TableCell>
        <a href="https://liveblocks.io/storage">Liveblocks Storage</a>
      </TableCell>
    </TableRow>
    <TableRow>
      <TableCell subtle={false}>
        Text and ordered sequences (editors, documents)
      </TableCell>
      <TableCell>
        <a href="https://liveblocks.io/yjs">Liveblocks Yjs</a>
      </TableCell>
    </TableRow>
    <TableRow>
      <TableCell subtle={false}>Both (Notion-style apps)</TableCell>
      <TableCell>Use both thoughtfully</TableCell>
    </TableRow>
  </TableBody>
</Table>

The techniques presented here represent battle-tested approaches from production
systems serving millions of users. While there are
[other sync engines](https://github.com/alangibson/awesome-crdt) and approaches
out there, the decision framework remains the same: match your data model to the
right conflict resolution strategy.

Now go build something collaborative. Reach out if you get stuck.

---

### Further Reading

- [Figma's Multiplayer Technology](https://www.figma.com/blog/how-figmas-multiplayer-technology-works/)
- [Scaling the Linear Sync Engine](https://linear.app/blog/scaling-the-linear-sync-engine)
- [Yjs Documentation](https://docs.yjs.dev/)
- [CRDT Research](https://crdt.tech/)
- [Liveblocks Documentation](/docs)
- [Linear-like issue tracker example](/examples/linear-like-issue-tracker/nextjs-linear-like-issue-tracker)
- [Conflict-Free Replicated Data Types (Wikipedia)](https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type)