How Macro's Sync Service Propagates Real-Time Updates Across Clients

Macro's sync service uses a Cloudflare Durable Object as the single source of truth, maintaining WebSocket connections to all clients and broadcasting CRDT-encoded updates within milliseconds using the Loro framework.

Macro is an open-source document editor that relies on a robust synchronization layer to ensure seamless real-time collaboration. At the heart of this system is a Cloudflare Durable Object that maintains authoritative document state while managing WebSocket connections for every participant. This article examines how the sync service in the macro-inc/macro repository achieves sub-second propagation of edits across distributed clients using a combination of CRDTs, binary message protocols, and resilient connection management.

The Architecture: Cloudflare Durable Objects as the Single Source of Truth

The sync service centers on a Cloudflare Durable Object that acts as the authoritative state holder for each document. When a client initiates collaboration, it connects to the WebSocket endpoint /document/{document_id}/connect, triggering the connect_handler function in services/sync-service/src/durable_object.rs.

Each incoming WebSocket receives a unique tag generated as a UUID, along with associated WebSocketMetadata stored within the Durable Object's memory. This tagging system allows the server to track individual client sessions and manage targeted broadcasts without confusion.

Connection Lifecycle and Initial Synchronization

Upon establishing a WebSocket connection, the server immediately evaluates whether a document snapshot exists. If available, the send_initial_sync function in services/sync-service/src/websocket.rs transmits a RemoteInitialSync message containing the shallow snapshot and current awareness data, bringing the new client to parity with existing participants.

If the document snapshot is not yet initialized, the sync process defers via the initialize_handler, queueing the initial state transmission until the document structure becomes available. This ensures clients never receive partial or corrupted initial states.

Processing Real-Time Updates with Loro CRDT

When a user edits the document, the client transmits a PeerUpdate message encoded using the Bebop binary protocol (FromPeer::PeerUpdate). The Durable Object processes this in three distinct phases:

  1. Import and Merge: The server calls DocumentState::import to apply the binary update to the Loro CRDT, which returns the specific Lexical node IDs touched by the operation.
  2. Operation Logging: The update is appended to the durable operation log via SessionStorage::append_pending_operation, ensuring persistence across Durable Object evictions.
  3. Acknowledgment and Broadcast: The server sends a RemoteUpdateAck to the originating client, then broadcasts the update to all other connected sockets via the broadcast loop within process_message.
// Server-side handling of PeerUpdate in durable_object.rs
FromPeer::PeerUpdate { updates, id } => {
    // Persist and merge the update into Loro CRDT
    let touched_nodes = session_storage
        .append_pending_operation(update, document_state)
        .await?;
    
    // Send acknowledgment to originator
    let ack = FromRemote::RemoteUpdateAck { id };
    ws.send_with_bytes(serialize(ack, &mut buf)?)?;
    
    // Broadcast to all other peers
    let sockets = dss.get_websockets();
    for w in sockets.iter().filter(|w| *w != ws) {
        w.send_with_bytes(serialize(
            FromRemote::RemoteUpdate { 
                update: SliceWrapper::Raw(update) 
            }, 
            &mut buf
        )?)?;
    }
}

Synchronizing Awareness and Cursor Positions

Beyond document content, Macro propagates ephemeral awareness data such as cursor positions and text selections. Clients send PeerAwareness messages containing their current selection state, which the Durable Object applies to an in-memory EphemeralStore.

The broadcast_awareness function then re-encodes and distributes this payload to all connected peers, ensuring every participant sees real-time cursor movements without polluting the permanent document history.

Durability and Persistence via Alarm Handlers

To prevent data loss during Durable Object eviction or worker restarts, the sync service implements a persistent storage mechanism using the Cloudflare Workers alarm API. The alarm handler in services/sync-service/src/durable_object.rs executes periodically based on the bump_alarm scheduling logic.

When DocumentState::should_save detects pending changes, the system:

  • Generates a new snapshot and stores it in the durable KV store
  • Clears applied operations from the pending queue
  • Invokes report_new_doc_state to notify external Macro services of the new document version

This alarm-driven approach balances write performance with durability, batching changes rather than performing expensive I/O on every keystroke.

Client-Side Resilience and Message Handling

The front-end implementation resides in packages/collaboration/src/sync-service/socket.ts, exporting the createSyncSocket function. This wrapper constructs a resilient WebSocket connection featuring Bebop binary serialization, exponential back-off for reconnection, and a manually-controlled heartbeat mechanism.

The client automatically queues outgoing updates during offline periods and decodes incoming FromRemote messages—including RemoteUpdate and RemoteAwareness—to apply changes to the local Loro document and awareness layer.

// Client-side sync socket implementation
import { createSyncSocket } from "./socket";
import { FromPeer, FromRemote } from "./generated/schema";

const ws = createSyncSocket(
  () => fetch("/api/sync-url").then(r => r.text())
);

ws.addEventListener("message", ({ data }) => {
  const msg = FromRemote.deserialize(data);
  switch (msg) {
    case FromRemote.RemoteUpdate:
      // Apply CRDT update to local Loro document
      loroDoc.import(msg.update);
      break;
    case FromRemote.RemoteAwareness:
      // Update remote cursor positions in awareness layer
      awareness.apply(msg.awareness);
      break;
  }
});

// Push local edits to the server
function pushUpdate(update: Uint8Array) {
  const peerMsg = FromPeer.PeerUpdate({ 
    updates: [update], 
    id: nextId() 
  });
  ws.send(peerMsg);
}

Summary

  • Single source of truth: Macro's sync service uses a Cloudflare Durable Object to maintain authoritative document state and manage all WebSocket connections via UUID-tagged sessions.
  • CRDT-based merging: The Loro CRDT (DocumentState::import) handles concurrent edits without conflicts, tracking touched Lexical nodes and ensuring semantic consistency across clients.
  • Binary messaging: Bebop-encoded messages (FromPeer, FromRemote) minimize payload size, with the server acknowledging updates via RemoteUpdateAck before broadcasting to peers.
  • Alarm-driven persistence: Periodic alarm handlers ensure durability by snapshotting document state to KV storage only when should_save returns true, optimizing for performance.
  • Resilient clients: The front-end createSyncSocket provides automatic reconnection, update queuing, and heartbeat management to survive network interruptions.

Frequently Asked Questions

How does Macro handle concurrent edits from multiple users?

Macro utilizes the Loro CRDT (Conflict-free Replicated Data Type) through the DocumentState::import method to merge concurrent updates. When multiple clients submit PeerUpdate messages simultaneously, the Durable Object imports each binary update into the Loro document, which automatically resolves conflicts based on causal relationships and node IDs. The system then broadcasts the merged state to all participants, ensuring eventual consistency without requiring operational transforms or locking mechanisms.

What happens when a client disconnects and reconnects?

The createSyncSocket wrapper in packages/collaboration/src/sync-service/socket.ts implements exponential back-off reconnection logic. Upon reconnecting to the /document/{document_id}/connect endpoint, the client receives a fresh RemoteInitialSync message containing the current document snapshot via send_initial_sync. Any local edits queued during the disconnection are transmitted as PeerUpdate messages once the WebSocket resumes, allowing the Durable Object to merge them into the current state via SessionStorage::append_pending_operation.

How does the sync service ensure data durability?

Durability is achieved through the Durable Object's alarm handler (alarm function in durable_object.rs). Rather than writing to storage on every update, the system accumulates changes in memory until DocumentState::should_save returns true. The alarm then persists a snapshot to the Cloudflare KV store and clears the operation log, ensuring that even if the Durable Object is evicted or the worker restarts, the document state can be reconstructed from the latest snapshot plus any subsequent operations.

What messaging protocol does Macro use for real-time communication?

Macro employs Bebop, a binary serialization format, for all WebSocket communications. The protocol defines two primary message enums: FromPeer for client-to-server updates (including PeerUpdate and PeerAwareness) and FromRemote for server-to-client broadcasts (including RemoteUpdate, RemoteAwareness, and RemoteUpdateAck). This binary approach reduces bandwidth consumption compared to JSON while providing type-safe, schema-driven message generation in both TypeScript and Rust.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →