Macro's CRDT-Based Real-Time Collaboration Architecture: A Deep Dive into Loro-Powered Document Sync
Macro's real-time collaboration engine uses a Loro-based CRDT architecture with a Write-Ahead Log (WAL), anti-entropy convergence, and pluggable transport layers to guarantee eventual consistency across peers.
Macro's collaborative editing system is built on a sophisticated CRDT (Conflict-Free Replicated Data Type) engine powered by the open-source Loro library. The macro-inc/macro repository implements a loosely-coupled architecture that delivers low-latency, always-online editing with automatic conflict resolution, durability, and live presence awareness. This article examines the core components, data flow, and convergence mechanisms that make this system production-ready.
Core Architectural Components
The collaboration stack in packages/collaboration/src/collab/ separates concerns into distinct, testable modules:
LoroManager: The CRDT Document Wrapper
The LoroManager (implemented in [manager.ts](https://github.com/macro-inc/macro/blob/main/packages/collaboration/src/collab/manager.ts)) wraps a raw Loro document and exposes a typed API for:
- Importing and exporting binary updates
- Resetting the document from a snapshot
- Converting between Loro's internal state and Macro's application-specific schema
This abstraction allows the rest of the system to remain agnostic of Loro's internal versioning format while still leveraging its CRDT properties.
SyncEngine: Central Orchestration
The SyncEngine ([engine.ts](https://github.com/macro-inc/macro/blob/main/packages/collaboration/src/collab/engine.ts)) is the heart of Macro's CRDT-based real-time collaboration. It coordinates:
- Local update capture and forwarding
- WAL persistence and remote broadcasting
- Remote update application and anti-entropy
- Periodic snapshot generation
- Awareness state management
The engine is idempotent—calling start() multiple times has no side effects, and reset() reinitializes from a fresh snapshot without losing unsaved local edits.
Write-Ahead Log (WAL) for Durability
The WAL ([wal.ts](https://github.com/macro-inc/macro/blob/main/packages/collaboration/src/collab/wal.ts)) guarantees that local edits survive crashes before server acknowledgment. Key characteristics:
- Appends raw Loro updates in order
- Can be replayed on startup if snapshots are stale
- Automatically pruned after successful snapshot persistence
Awareness for Presence
Awareness ([awareness.ts](https://github.com/macro-inc/macro/blob/main/packages/collaboration/src/collab/awareness.ts)) handles lightweight, ephemeral data:
- Cursor positions and text selections
- User identity (name, avatar)
- Per-peer state that merges without conflicts
Awareness updates are encoded, broadcast over the same channel as document edits, and merged locally via awareness.importRemoteAwareness.
SyncService: Pluggable Transport
The sync layer provides two concrete sources ([source.ts](https://github.com/macro-inc/macro/blob/main/packages/collaboration/src/collab/source.ts)):
LiveSyncSource: WebSocket-based transport to the Macro backendWALSyncer: Persistent log replay for offline-first recovery
This abstraction enables alternative transports—Cloudflare Workers, server-side AI editors, or custom backends—without modifying the core engine.
SnapshotStore for Fast Recovery
The SnapshotStore ([snapshot-store.ts](https://github.com/macro-inc/macro/blob/main/packages/collaboration/src/collab/snapshot-store.ts)) persists shallow Loro snapshots every 5 seconds (SNAPSHOT_INTERVAL_MS). Implementations target S3, IndexedDB, or in-memory storage depending on the environment.
Chatter for Intra-Tab Sync
Chatter ([chatter.ts](https://github.com/macro-inc/macro/blob/main/packages/collaboration/src/collab/chatter.ts)) enables zero-latency sync between browser tabs editing the same document. The default BroadcastChannelChatter can be replaced via the makeChatter factory for other messaging systems.
Data Flow Through the System
A single edit traverses the architecture as follows:
- Local capture:
loroManager.doc.subscribeLocalUpdatesdetects changes - WAL append:
syncs.wal.appenddurably logs the update - Chatter broadcast:
chatter.post({type:'update', data})notifies other tabs immediately - Server sync:
LiveSyncSourceforwards via WebSocket; server persists to durable storage - Remote application:
engine.handleRemoteUpdateimports peer changes into the Loro document - Anti-entropy trigger: If prerequisite operations are missing,
convergeFromServerrequests catch-up state - Snapshot cycle: Every 5 seconds,
persistSnapshotexportsdoc.export({mode:'shallow-snapshot'})and prunes covered WAL entries
Anti-Entropy and Convergence
CRDTs guarantee that concurrent edits commute, but anti-entropy ensures all peers eventually receive the same operations. The convergence mechanism in [engine.ts](https://github.com/macro-inc/macro/blob/main/packages/collaboration/src/collab/engine.ts) uses a generation counter to coordinate requests:
private convergeFromServer({ rerunIfInFlight = false } = {}): Promise<void> {
const generation = this.lifecycleGeneration;
if (!this.isActiveGeneration(generation)) return Promise.resolve();
const inFlight = this.convergence;
if (inFlight?.generation === generation) {
if (rerunIfInFlight) inFlight.rerunRequested = true;
return inFlight.promise;
}
const since = this.loroManager.doc.version(); // current version vector
const promise = this.requestAndHandleUpdatesSince(since, 1, generation);
this.convergence = { generation, promise, rerunRequested: false };
// ...
}
requestUpdatesSincefetches all operations after the client's version vector- Retries execute up to
REQUEST_UPDATES_MAX_ATTEMPTS(3) with exponential backoff - Generation tracking prevents stale updates from interfering after
stop()/start()cycles
Persistence and Crash Recovery
The dual-layer persistence strategy balances speed and durability:
| Layer | Purpose | Trigger |
|---|---|---|
| WAL | Immediate durability for in-flight edits | Every local update |
| Snapshot | Fast recovery, historical truncation | Every SNAPSHOT_INTERVAL_MS (5s) |
On startup, the engine:
- Loads the latest snapshot via
defaultSnapshotThunk - Replays WAL entries newer than the snapshot
- Enters normal sync operation
Snapshots use Loro's efficient binary format: doc.export({mode:'shallow-snapshot', frontiers:doc.oplogFrontiers()}).
Practical Implementation
Below is a minimal integration showing how to instantiate Macro's CRDT-based real-time collaboration engine:
import { createSyncEngine } from '@macro-inc/collaboration/collab/engine';
import { createLoroManager } from '@macro-inc/collaboration/collab/manager';
import { createAwareness } from '@macro-inc/collaboration/collab/awareness';
import { createLiveSyncSource } from '@macro-inc/collaboration/sync-service/source';
import { createWALSyncer } from '@macro-inc/collaboration/collab/wal';
import { createSnapshotStore } from '@macro-inc/collaboration/collab/snapshot-store';
// 1. Initialise low-level components
const loroManager = createLoroManager({ schema: MyDocumentSchema });
const awareness = createAwareness({ userId: currentUser.id, name: currentUser.name });
const liveSource = createLiveSyncSource({ documentId: docId, url: WS_URL, token: authToken });
const walSyncer = createWALSyncer({ storage: indexedDB });
const snapshotStore = createSnapshotStore({ bucket: 'macro-snapshots' });
// 2. Build the reactive engine
const syncEngine = createSyncEngine({
loroManager,
awareness,
syncs: { live: liveSource, wal: walSyncer },
bindings: {
onRemoteState: (state) => {
editor.setContent(state);
},
},
snapshotStore,
});
// 3. Start collaboration
syncEngine.start();
// 4. Push local changes
editor.onChange((newState) => {
syncEngine.syncStateToLoro(newState);
});
// 5. Clean shutdown
window.addEventListener('beforeunload', () => syncEngine.stop());
The returned syncEngine exposes reactive state (isRunning), lifecycle methods (start, stop, reset), and synchronization hooks (syncStateToLoro, syncAwarenessToLoro).
Scaling and Extensibility
Macro's architecture intentionally separates concerns to support diverse deployment scenarios:
- Transport-agnostic: Replace
LiveSyncSourcefor WebRTC, WebTransport, or serverless edge functions - Custom Chatter: Inject
makeChatterfor Electron IPC, SharedWorker, or Node.js cluster messaging - Snapshot backends: Implement
SnapshotStore<RawUpdate>for Redis, PostgreSQL, or tiered storage
This modularity enables the same codebase to power browser-based editors, AI-assisted writing agents, and server-side document processors.
Summary
- LoroManager wraps the CRDT document with schema-aware serialization in [
manager.ts](https://github.com/macro-inc/macro/blob/main/packages/collaboration/src/collab/manager.ts) - SyncEngine orchestrates the full sync lifecycle with idempotent start/stop semantics in [
engine.ts](https://github.com/macro-inc/macro/blob/main/packages/collaboration/src/collab/engine.ts) - WAL + SnapshotStore provide crash recovery and historical truncation via [
wal.ts](https://github.com/macro-inc/macro/blob/main/packages/collaboration/src/collab/wal.ts) and [snapshot-store.ts](https://github.com/macro-inc/macro/blob/main/packages/collaboration/src/collab/snapshot-store.ts) - Anti-entropy with version vectors and retry logic ensures convergence despite network partitions
- Modular transport layers allow the same core to run in browsers, workers, and server environments
Frequently Asked Questions
How does Macro's CRDT-based real-time collaboration handle offline editing?
The WAL persists every local update to IndexedDB or similar storage immediately. When connectivity returns, the WALSyncer replays buffered operations and the anti-entropy loop fetches any missing remote changes. The Loro CRDT guarantees that concurrent offline edits merge correctly without user intervention.
What happens if two users edit the same text simultaneously?
Loro's Reg (register) and Text CRDT types automatically resolve conflicts using last-writer-wins or multi-value semantics depending on the schema. The SyncEngine applies remote updates via loroManager.importUpdate, and the CRDT's commutative properties ensure all peers converge to identical document states regardless of operation ordering.
Can the sync engine run entirely without a server?
Yes, for single-user or local-network scenarios. The BroadcastChannelChatter enables tab-to-tab sync without WebSocket infrastructure. However, server persistence via LiveSyncSource is required for cross-device synchronization and durable history. The modular design allows either mode without code changes.
How often does Macro snapshot document state?
The engine snapshots every 5 seconds (SNAPSHOT_INTERVAL_MS) by default, as defined in [engine.ts](https://github.com/macro-inc/macro/blob/main/packages/collaboration/src/collab/engine.ts). This interval balances recovery speed against storage costs. After a successful snapshot, WAL entries covered by that snapshot are pruned to reclaim space.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →