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

> Discover how Macro's sync service achieves real-time updates using Cloudflare Durable Objects and the Loro framework for lightning-fast CRDT propagation.

- Repository: [Macro/macro](https://github.com/macro-inc/macro)
- Tags: internals
- Published: 2026-08-18

---

**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`](https://github.com/macro-inc/macro/blob/main/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`](https://github.com/macro-inc/macro/blob/main/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`.

```rust
// 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`](https://github.com/macro-inc/macro/blob/main/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`](https://github.com/macro-inc/macro/blob/main/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.

```typescript
// 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`](https://github.com/macro-inc/macro/blob/main/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`](https://github.com/macro-inc/macro/blob/main/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.