• DocsDocs
  • PricingPricing
Sign in
Get started
Sign in
Get started
    • Ready-made features
      • Comments
        Comments

        Contextual commenting

      • Multiplayer
        Multiplayer

        Realtime collaboration

      • AI Agents
        AI Agents

        Collaborative AI agents

      • Notifications
        Notifications

        Smart alerts for your app

    • Platform
      • Collaboration Infrastructure
        Collaboration Infrastructure

        The engine behind multiplayer apps

      • DevTools
        DevTools

        Browser extension

    • Tools
      • Examples

        Gallery of open source examples

      • Showcase

        Gallery of collaborative experiences

      • Next.js Starter Kit

        Kickstart your Next.js collaborative app

      • Tutorial

        Step-by-step interactive tutorial

      • Guides

        How-to guides and tutorial

      • Figma UI Kit

        Liveblocks Collaboration Kit

    • Company
      • Blog

        The latest from Liveblocks

      • Customers

        The teams Liveblocks empowers

      • Changelog

        Weekly product updates

      • Security

        Our approach to security

      • About

        The story and team behind Liveblocks

  • Docs
  • Pricing
  • Ready-made features
    • Comments
    • Multiplayer
    • AI Agents
    • Notifications
    Platform
    • Collaboration Infrastructure
    • DevTools
    Solutions
    • People platforms
    • Sales tools
    • Startups
    Use cases
    • Multiplayer forms
    • Multiplayer text editor
    • Multiplayer creative tools
    • Multiplayer whiteboard
    • Comments
    • Sharing and permissions
    • Document browsing
  • Resources
    • Documentation
    • Examples
    • Showcase
    • React components
    • Next.js Starter Kit
    • Tutorial
    • Guides
    • Release notes
    Technologies
    • Next.js
    • React
    • JavaScript
    • Redux
    • Zustand
    • Yjs
    • Tiptap
    • BlockNote
    • Slate
    • Lexical
    • Quill
    • Monaco
    • CodeMirror
  • Company
    • Pricing
    • Blog
    • Customers
    • Changelog
    • About
    • Contact us
    • Careers
    • Terms of service
    • Privacy policy
    • DPA
    • Security
    • Trust center
    • Subprocessors
  • HomepageSystem status
    • Github
    • Discord
    • X
    • LinkedIn
    • YouTube
    © 2025 Liveblocks Inc.
Blog/Product & Design, Engineering

Understanding sync engines: How Figma, Linear, and Google Docs work

Learn how different realtime sync engines work, dive into the details of Yjs and CRDTs, and discover which collaboration infrastructure is right for your application.

on December 17th
Understanding sync engines: How Figma, Linear, and Google Docs work
December 17th·19 min read
Share article

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 to event-driven microservice architectures—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, Linear, and products built with Liveblocks like Vercel, Hashnode, and Resend.

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.

A custom code block with syntax highlighting, inside an AI chat

Property-level uses last-write-wins per property, where character-level merges based on position.

Concrete Examples

Two users are editing a task item while offline:

const task = {  title: "Buy apples",  done: false,};
const changeA = {  attribute: "title",  value: "Buy apples and oranges",  timestamp: "2024-01-15T10:30:00Z",};
const changeB = {  attribute: "done",  value: true,  timestamp: "2024-01-15T10:30:05Z",};
// Both changes survive because they modified different propertiesconst 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:

Buy apples
Buy apples `and oranges`
Buy apples `and bananas`
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 is one of the most well-documented collaborative architectures. How it works:

type FigmaDocument = Map<ObjectID, Map<Property, Value>>;
// Example: A rectangle objectconst 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. 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.

“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.”
Image of Figma
Image of Evan Wallace
Evan WallaceCo-founder, 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. 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:

ReasonImpact

Design tools work with discrete objects

Each shape, layer, or component is an independent entity

Properties update independentlyColor changes don’t conflict with position changes

Users rarely edit same property simultaneously

Natural work patterns reduce conflicts
Last-write-wins is intuitive

When conflicts occur, newest wins matches user expectations

Significantly simpler than full CRDTs

Faster performance, easier to debug

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 and reverse-engineered here) and uses a similar property-level approach for most data:

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).

Linear Design Choice

Rationale
Event-based architectureEvery change is a separate event with metadata
Property-level granularityStatus changes don't conflict with title edits
Server-ordered sync IDs

Reliable ordering through centralized transaction processing

Last-Write-Wins defaultConflicts are rare; simple resolution works
CRDTs only for rich textAdded later for issue descriptions only

According to their engineering team's discussions, 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:

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 to decide which text comes first based on these identifiers:

`Hello world`
`Hi there`
`Hello world` OR `Hi there`
`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.

Use Case Category

ExamplesWhy property-level works
Visual/spatial editorsFigma, Miro, Whimsical, Lucidchart

Objects have discrete properties (position, size, color)

Diagramming toolsDraw.io, flowchart buildersNodes and edges are independent objects
Structured data appsKanban boards, form builders, spreadsheet cells

Each item has distinct properties that rarely conflict

Configuration toolsDashboard builders, workflow designers

Changes typically affect different objects/properties

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 is a CRDT-like data structure that allows these changes to be merged automatically.

import { LiveObject } from "@liveblocks/client";
// Create a shape, each property is independently mutableconst rectangle = new LiveObject({ type: "rectangle", x: 0, y: 200, fill: "blue",});
// User A moves the rectanglerectangle.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 and 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 and our text editor integrations.

Use Case Category

ExamplesWhy Character-level Is required
Text/rich-text editorsGoogle Docs, Notion, MediumMultiple users typing in same paragraph
Code editorsVSCode Live Share, ReplitCharacter-level precision for code
Markdown editorsHackMD, CodiMDText content is primary data
Messaging/commentsSlack-like apps with rich textInsertion order and position matter

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 is a CRDT-like data structure that allows these changes to be merged automatically.

const yText = yDoc.getText("content");
// User A types at position 0yText.insert(0, "Hello");
// User B types at position 0 simultaneouslyyText.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.

A custom code block with syntax highlighting, inside an AI chat

What actually matters in production

After studying production systems:

Technique

Why It MattersWhen to Use
Property-level LWWMatches user mental models for object editing90% of design tools, structured data apps
Character-level CRDTsRequired for concurrent text editingAny collaborative text/code editor
Logical clocks

System timestamps are unreliable in distributed systems

Distributed systems without central ordering authority

Optimistic updatesUsers expect instant feedbackEvery realtime collaborative app
Presence/awarenessSeeing others' cursors is table stakesApps with multiple simultaneous editors

What doesn't matter as much:

  • OT vs CRDT debate: Yjs and CRDTs won. Use CRDTs.
  • P2P vs client-server: Client-server is simpler and works fine for most use cases.
  • Custom CRDT implementations: The research is solved. 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.

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 timestampfunction 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 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:

-- Naive approach that doesn't scaleCREATE TABLE changes (  id BIGSERIAL PRIMARY KEY,  document_id UUID,  attribute TEXT,  value JSONB,  timestamp BIGINT);
-- Production approachCREATE 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.

Component

ComplexityWhy it’s hard
Logical clocksHigh

HLCs require careful implementation; bugs cause silent data corruption

Database SchemaMedium

Must handle millions of events, efficient queries, garbage collection

Offline SyncVery High

Queuing, reconnection, merge conflicts, UI updates during merge

Presence SystemMedium

Separate broadcast system for cursors, selections, typing indicators

Permission ControlHigh

Row-level security, encryption, audit logs, graceful access revocation

WebSocket InfrastructureVery High

Connection pooling, load balancing, heartbeats, mobile sleep/wake handling

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? 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:

import { createClient } from "@liveblocks/client";import { LiveObject, LiveList, LiveMap } from "@liveblocks/client";
const client = createClient({ publicApiKey: "pk_prod_...",});
// Enter a room with typed storageconst { 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 changesroom.subscribe(room.getStorage().root, () => { // React to any storage change const shapes = room.getStorage().root.get("shapes"); renderCanvas(shapes);});
// Make changes that sync automaticallyconst 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 propertiesshape.set("x", 150); // Only x syncs, not the whole object

Key capabilities:

  • LiveObject, LiveMap, LiveList CRDT-like data structures.
  • Automatic conflict resolution per property.
  • Optimistic updates with automatic sync.
  • Connection handling with offline detection.
  • Built-in presence (cursors, selections).
  • Packages for React, Zustand, Redux.

Liveblocks Yjs: Character-level sync

Architecture pattern:

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 documentconst yDoc = new Y.Doc();const yText = yDoc.getText("content");
// Connect to Liveblocksconst provider = new LiveblocksProvider(room, yDoc);
// Bind to editorconst 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.
  • Managed WebSocket infrastructure.
  • Persistent storage for Yjs documents.
  • Easy integrations for popular text editors: Lexical, Tiptap, BlockNote.
  • REST API and Node.js packages for server-side modifications.

Using both together

Many applications need both:

// Notion-style app: Storage for structure, Yjs for content
// Room setup with bothconst 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 documentconst 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, Linear, Yjs).

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:

If you're building...

Use...

Objects with properties (design tools, kanban boards)

Liveblocks Storage

Text and ordered sequences (editors, documents)

Liveblocks Yjs
Both (Notion-style apps)Use both thoughtfully

The techniques presented here represent battle-tested approaches from production systems serving millions of users. While there are other sync engines 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
  • Scaling the Linear Sync Engine
  • Yjs Documentation
  • CRDT Research
  • Liveblocks Documentation
  • Linear-like issue tracker example
  • Conflict-Free Replicated Data Types (Wikipedia)
CollaborationSync EnginesLiveblocks StorageYjsCRDTs

Ready to get started?

Join thousands of companies using Liveblocks ready‑made collaborative features to drive growth in their products.

Get started for free

Related blog posts

  • Configure each user’s notification settings for email, Slack, and more

    Configure each user’s notification settings for email, Slack, and more

    Picture of Chris Nicholas
    March 6th
    Product & Design
  • A better way to email your users about unread content

    A better way to email your users about unread content

    Picture of Chris Nicholas
    December 18th, 2024
    Product & Design
  • Increase in-app commenting with attachments

    Increase in-app commenting with attachments

    Picture of Chris Nicholas
    October 3rd, 2024
    Product & Design