DEV Community

Cover image for How Figma Does Real-Time Sync (And Now You Can Too)
Francisco Molina
Francisco Molina

Posted on

How Figma Does Real-Time Sync (And Now You Can Too)

Building Figma-like real-time collaboration for your app? Stop fighting Redux/Zustand. We built Syncwave: distributed state management with automatic conflict resolution. No merge conflicts, ever.


The Problem Figma Solved (That Nobody Talks About)

Ever wonder how Figma handles this scenario:

Alice edits a component while Bob renames it at the same millisecond.

No conflict. No "pick your version" dialog. Both clients just... converge.

Alice's action: set component.name = 'Button'
Bob's action:   set component.size = 'large'

Figma: Both happen. Zero conflict.
Enter fullscreen mode Exit fullscreen mode

Redux can't do this.
Zustand can't do this.
MobX can't do this.

We built SYNCWAVE because we needed it, and we couldn't find it anywhere.


The Dirty Truth About Redux + Real-Time

Redux handles local state beautifully.

But add a second client? It falls apart:

// Client A: Redux
store.dispatch({ type: 'SET_NAME', name: 'Alice' })

// Client B: Redux (at the same time)
store.dispatch({ type: 'SET_SIZE', size: 'large' })

// What happens?
// → One change wins
// → One change is lost
// → Sync it somehow? (websocket?)
// → Merge conflicts on the server
// → No automatic resolution
Enter fullscreen mode Exit fullscreen mode

Figma doesn't have this problem. It uses CRDTs (Conflict-free Replicated Data Types).


What Are CRDTs? (And Why They Matter)

A CRDT is math that ensures: No matter the order of changes, all clients converge to the same state.

Client A sees:     Client B sees:
count = 10         count = 10
  ↓ (add 5)          ↓ (add 3)
count = 15         count = 13

Without CRDT:
→ Conflict! Which is right?

With CRDT:
→ Both converge to: count = 13 + 5 = 18
→ Because increment is commutative
Enter fullscreen mode Exit fullscreen mode

Syncwave uses CRDTs so every client automatically converges, no matter what order changes arrive in.


What Syncwave Actually Does

1. Real-Time Sync (No Polling)

Changes replicate instantly to all connected clients:

// Client A
store.set('user.name', 'Alice');

// Client B (instantly)
// automatically gets: user.name = 'Alice'
// No refetch. No polling. No websocket code.
Enter fullscreen mode Exit fullscreen mode

2. Automatic Conflict Resolution

When two clients edit simultaneously:

// Client A
store.set('count', 5);

// Client B (same millisecond)
store.set('count', 10);

// Result on both clients: 10
// No dialog. No merge. Just... resolved.
Enter fullscreen mode Exit fullscreen mode

3. Offline-First

Work without internet. Changes sync automatically when you're back online.

// Phone is offline
store.set('todos', [{ text: 'Learn Syncwave' }]);

// Still works! Changes queued locally.

// Back online?
// Changes replay. CRDT resolves any conflicts with server.
Enter fullscreen mode Exit fullscreen mode

4. Undo/Redo Everywhere

Revert actions across all clients in real-time:

store.set('name', 'Alice');
store.set('name', 'Bob');

store.undo(); // Name back to 'Alice'
// Every client sees this instantly

store.redo(); // Name back to 'Bob'
Enter fullscreen mode Exit fullscreen mode

5. Full History (Event Sourcing)

Every change is recorded. Rebuild state from any point:

const history = store.getHistory();
// [
//   { type: 'set', path: 'name', value: 'Alice', timestamp: 123 },
//   { type: 'set', path: 'name', value: 'Bob', timestamp: 124 },
// ]

// Replay to timestamp 123:
const stateAt123 = store.replay(initialState, 0, 123);
// state.name = 'Alice'
Enter fullscreen mode Exit fullscreen mode

How It Compares

Feature Redux Zustand Syncwave
Local state
Real-time sync
Offline support
Auto conflict resolution
Undo/Redo
Event history
File size 40KB 2KB 5KB
Learning curve Steep Moderate Easy

Real Example: Collaborative Todo App

Without Syncwave (Redux):

// Setup
const store = createStore({
  todos: [],
});

// Add todo on Client A
store.dispatch({ type: 'ADD_TODO', text: 'Learn Redux' });

// Add todo on Client B (same time)
store.dispatch({ type: 'ADD_TODO', text: 'Learn WebSockets' });

// Now what?
// → Manual websocket code
// → Server merges or picks one
// → One todo might be lost
// → Users get confused
Enter fullscreen mode Exit fullscreen mode

With Syncwave:

// Setup
const store = createStore({
  todos: [],
});

// Add todo on Client A
store.set('todos', [
  { id: 1, text: 'Learn Syncwave', done: false }
]);

// Add todo on Client B (same time)
store.set('todos', [
  { id: 2, text: 'Build real-time app', done: false }
]);

// Result on both clients:
// ✅ Both todos exist
// ✅ No manual merge code
// ✅ No server sync logic
// ✅ Works offline too
Enter fullscreen mode Exit fullscreen mode

Installation

npm install syncwave
Enter fullscreen mode Exit fullscreen mode

Quick Start

Basic Usage

import { createStore } from 'syncwave';

const store = createStore({
  user: { name: 'Alice', email: 'alice@example.com' },
  todos: [],
  count: 0,
});

// Subscribe to changes
store.subscribe((newState, oldState) => {
  console.log('State updated:', newState);
});

// Update state
store.set('user.name', 'Bob');
store.set('count', 42);

// Get current state
const state = store.getState();
Enter fullscreen mode Exit fullscreen mode

Offline-First + Undo/Redo

const store = createStore(
  { todos: [] },
  {
    offline: true,    // Enable offline persistence
    undoRedo: true,   // Enable undo/redo
  }
);

// Works offline
store.set('todos', [{ id: 1, text: 'Learn' }]);

// Back online? Auto-syncs.

store.undo(); // Reverts last change
Enter fullscreen mode Exit fullscreen mode

Real-Time Sync (Multi-Client)

// Client A
const storeA = createStore({ data: 'initial' });

// Client B
const storeB = createStore({ data: 'initial' });

// Listen for events
storeA.onEvent((event) => {
  // Send to server/broadcast
  broadcastChannel.postMessage(event);
});

storeB.onEvent((event) => {
  broadcastChannel.postMessage(event);
});

// Handle incoming events
broadcastChannel.onmessage = (e) => {
  storeA.importEvents([e.data]);
  storeB.importEvents([e.data]);
};

// Now edit simultaneously:
storeA.set('data', 'from-A');
storeB.set('data', 'from-B');

// Both converge automatically ✨
Enter fullscreen mode Exit fullscreen mode

Undo/Redo

store.set('count', 1);
store.set('count', 2);
store.set('count', 3);

console.log(store.getValue('count')); // 3

store.undo();
console.log(store.getValue('count')); // 2

store.undo();
console.log(store.getValue('count')); // 1

store.redo();
console.log(store.getValue('count')); // 2
Enter fullscreen mode Exit fullscreen mode

Event History & Time Travel

const history = store.getHistory();
// [
//   { type: 'set', path: 'count', value: 1, timestamp: 123 },
//   { type: 'set', path: 'count', value: 2, timestamp: 124 },
// ]

// Replay to any version
const stateAtVersion = store.eventLog.replay(initialState, 0, 5);
Enter fullscreen mode Exit fullscreen mode

Why This Matters

Apps like Figma, Notion, Linear, Slack all solve the same problem:

How do we keep multiple clients in sync, with zero conflicts?

They use CRDTs. Now you can too, without building it from scratch.

Real-World Use Cases

  1. Collaborative Editing (like Google Docs)

    • Multiple users editing simultaneously
    • No "version conflict" dialogs
  2. Real-Time Dashboards

    • Multiple viewers seeing live data
    • Automatic convergence
  3. Multiplayer Apps (like Figma)

    • Drag an element while someone else renames it
    • Both changes apply
  4. Offline-First Mobile

    • Work on your phone
    • Auto-sync when back online
    • No data loss
  5. Undo/Redo Across Devices

    • Undo on phone
    • See it reflected on desktop
    • Across network

Technical Deep Dive

How CRDTs Work

Syncwave uses Last-Write-Wins (LWW) CRDTs with timestamp-based resolution:

// Both changes happen simultaneously:
// Client A (timestamp 1000): set count = 5
// Client B (timestamp 1001): set count = 10

// CRDT automatically chooses:
// Timestamp 1001 wins → count = 10
// No conflict dialog needed
Enter fullscreen mode Exit fullscreen mode

For more complex operations, Syncwave uses Conflict-free Replicated Data Types that guarantee mathematical convergence.

Event Log

Every change is immutable:

{
  type: 'set',
  path: 'user.name',
  value: 'Alice',
  timestamp: 1234567890,
  clientId: 'client-abc',
  version: 5
}
Enter fullscreen mode Exit fullscreen mode

This enables:

  • Full audit trail
  • Time-travel debugging
  • Replay from any point

Memory-Bounded

Syncwave is optimized for performance:

  • ~5KB gzipped (including all features)
  • Configurable history limit
  • Efficient diffs (only send changes)
  • No external dependencies

TypeScript Support

Full type safety:

interface State {
  user: { name: string; email: string };
  todos: Array<{ id: number; text: string }>;
}

const store = createStore<State>({
  user: { name: 'Alice', email: 'alice@example.com' },
  todos: [],
});

store.set('user.name', 'Bob'); // ✅
store.set('user.age', 30);     // ❌ Type error!
Enter fullscreen mode Exit fullscreen mode

Browser Support

  • Chrome 60+
  • Firefox 60+
  • Safari 12+
  • Edge 79+

Also works with Node.js for server-side state management.


Roadmap

  • 🔜 WebSocket sync adapter
  • 🔜 React hooks (useStore, useSubscribe)
  • 🔜 Vue composables
  • 🔜 Svelte stores
  • 🔜 Time-travel debugger UI
  • 🔜 Encrypted sync
  • 🔜 Conflict visualization

Try It Now

npm install syncwave
Enter fullscreen mode Exit fullscreen mode

Then, 5 minutes from now, you'll have multi-device state management.


The Philosophy

Redux/Zustand solve local state.

Syncwave solves distributed state.

If you're building:

  • ✅ Collaborative apps
  • ✅ Real-time multiplayer
  • ✅ Offline-first apps
  • ✅ Apps with multiple clients

You need Syncwave.


Links


Final Thought

Figma didn't build their real-time sync from scratch. They based it on decades of CRDT research.

Now you don't have to either.

Syncwave is that research, packaged into 5KB of library.

Use it. Build awesome things. No conflicts, ever.


Have questions? Open an issue on GitHub or ask away in the comments.

Made with ❤️ by developers building real-time apps.


If you found this helpful, share with your team! Especially if you're:

  • Building collaborative software
  • Scaling multiplayer apps
  • Working on offline-first mobile
  • Tired of Redux boilerplate

Top comments (0)