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.
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
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
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.
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.
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.
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'
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'
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
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
Installation
npm install syncwave
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();
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
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 ✨
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
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);
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
-
Collaborative Editing (like Google Docs)
- Multiple users editing simultaneously
- No "version conflict" dialogs
-
Real-Time Dashboards
- Multiple viewers seeing live data
- Automatic convergence
-
Multiplayer Apps (like Figma)
- Drag an element while someone else renames it
- Both changes apply
-
Offline-First Mobile
- Work on your phone
- Auto-sync when back online
- No data loss
-
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
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
}
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!
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
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
- GitHub: github.com/frxcisxo/syncwave
- NPM: @frxncisxo/syncwave
- Docs: syncwave.dev
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)