# How Agent-Native Handles Real-Time Collaboration with Yjs CRDT

> Discover how Agent-Native achieves seamless real-time collaboration using Yjs CRDT. Explore its robust server client architecture for instant updates and presence.

- Repository: [Builder.io/agent-native](https://github.com/BuilderIO/agent-native)
- Tags: deep-dive
- Published: 2026-06-28

---

**Agent-Native implements a full-stack real-time collaboration layer using Yjs CRDT, combining a server-side LRU-cached document manager with atomic write locks and a client-side React hook that manages debounced updates, SSE/polling hybrid sync, and presence awareness.**

Agent-Native is an open-source framework that enables real-time collaborative editing through a robust integration with the Yjs CRDT library. The architecture splits responsibilities between a persistent server-side document manager and a sophisticated client-side synchronization hook. This design ensures conflict-free concurrent editing with low latency across multiple users and AI agents.

## Server-Side Yjs Document Lifecycle and Persistence

The server-side implementation in [`packages/core/src/collab/ydoc-manager.ts`](https://github.com/BuilderIO/agent-native/blob/main/packages/core/src/collab/ydoc-manager.ts) manages Yjs document instances, persistence, and concurrent access control.

### LRU Cache and Document Retrieval

The system maintains a global `_cache` Map that holds up to **50** `Y.Doc` instances defined by `MAX_CACHE`. When a cache miss occurs, the manager calls `loadYDocState` to fetch the document from the SQL `_collab_docs` table and stores it for fast subsequent reads.

### Atomic Write Locks

Concurrent writes for the same `docId` are serialized using `_writeLocks`. The `withDocWriteLock` function ensures that only one operation modifies a document at a time, preventing race conditions during high-concurrency scenarios.

### Update Processing and State Compaction

The `applyUpdate` function receives binary Yjs updates from clients and applies them using `Y.applyUpdate`. After merging, `persistMergedState` saves the state to the database. To prevent unbounded growth, the system automatically **compacts** the persisted blob when it exceeds **4×** the size of a fresh encoding, stripping tombstones and optimizing storage.

### Incremental Synchronization

Clients can request partial updates via `getIncUpdate`, which accepts a client state vector and returns only missing operations using `Y.encodeStateAsUpdate(doc, clientStateVector)`. This minimizes bandwidth for catch-up scenarios.

## Text-to-Yjs Diffing

Plain-text changes are converted to minimal CRDT operations in [`packages/core/src/collab/text-to-yjs.ts`](https://github.com/BuilderIO/agent-native/blob/main/packages/core/src/collab/text-to-yjs.ts). The `applyTextToYDoc` function utilizes the `diff-match-patch` algorithm to compute character-level differences between the current and new text, then generates precise insert and delete operations on the `Y.Text` type.

## Client-Side Collaboration with useCollaborativeDoc

The client implementation centers on the `useCollaborativeDoc` hook in [`packages/core/src/collab/client.ts`](https://github.com/BuilderIO/agent-native/blob/main/packages/core/src/collab/client.ts), which orchestrates document initialization, synchronization, and presence management.

### Stable Document Instance

The hook uses `useMemo` to create a single `Y.Doc` instance for each `docId`. This stability is required for compatibility with TipTap’s Collaboration extension, ensuring the same document reference survives React re-renders.

### Update Debouncing and Coalescing

Local edits are buffered and merged using `Y.mergeUpdates` after an **80ms** debounce before posting to the `/update` endpoint. The buffer automatically flushes on page hide, visibility changes, or before each poll cycle to guarantee no edits are lost.

### Hybrid Sync Strategy

The client implements a dual-path synchronization mechanism:

- **SSE Fast-Path**: When `EventSource` is available, the hook subscribes to `/events` and applies incoming updates immediately via `Y.applyUpdate`.
- **Polling Fallback**: Every **2 seconds** (or **12 seconds** when SSE is healthy), the client polls `/_agent-native/poll` for batched events. If the server’s ring-buffer overflow is detected, the client performs a full state-vector fetch to reconcile missed operations.

### Awareness and Presence

User presence—including name, email, and cursor color—is tracked using a `Y-protocols/awareness` instance. Local awareness state is pushed to the server via a throttled POST (`scheduleAwarenessPush`) every **~150ms**, while remote awareness updates are received through SSE or polling and rendered as cursor indicators.

### Leader Election for External Snapshots

When merging external snapshots (such as AI-generated content), the system uses `isReconcileLeadClient` to elect a single leader. Only the visible client with the lowest Yjs `clientID` applies the snapshot. The agent’s client ID is explicitly set to `Number.MAX_SAFE_INTEGER` to ensure it never becomes the lead, guaranteeing deterministic conflict resolution.

### Visibility-Aware Polling

Hidden browser tabs pause polling and publish `visible:false` in their awareness payload, allowing other tabs to assume leadership. When a tab becomes visible again, it immediately publishes its state and flushes any pending local updates.

## End-to-End Collaboration Flow

1. **Initial Load**: The client fetches the full Yjs state from `/state` and applies it using `Y.applyUpdate`.
2. **Local Edit**: User input triggers `applyTextToYDoc` to compute a diff, Yjs generates a binary update, and the client buffers and posts it to the server.
3. **Remote Edit**: Another client sends an update, the server stores it in `_collab_docs`, and the update is delivered via SSE or poll to all connected clients.
4. **Presence**: Each client continuously posts awareness payloads; peers receive these via SSE/poll and update UI cursor indicators in real time.
5. **Persistence**: All updates are persisted to the SQL `_collab_docs` table, enabling new clients to load the current state instantly.

## Code Examples

```tsx
// Hook usage in a Rich-Markdown editor component
import { useCollaborativeDoc } from "@agent-native/core/client";
import { RichMarkdownEditor } from "@agent-native/core/client/rich-markdown-editor";

function MyEditor({ docId, user }) {
  const {
    ydoc,
    awareness,
    isLoading,
    isSynced,
    activeUsers,
    agentActive,
    agentPresent,
  } = useCollaborativeDoc({ docId, user });

  if (isLoading) return <Spinner />;
  return (
    <RichMarkdownEditor
      ydoc={ydoc!}
      awareness={awareness!}
      placeholder="Start typing…"
    />
  );
}

```

```ts
// Server-side endpoint that receives a client update
import { applyUpdate } from "@agent-native/core/collab/ydoc-manager";

export async function POST(req: Request, { params }: { params: { docId: string } }) {
  const { update, requestSource } = await req.json();
  const binary = Uint8Array.from(atob(update), c => c.charCodeAt(0));
  await applyUpdate(params.docId, binary, requestSource);
  return new Response(null, { status: 204 });
}

```

```ts
// Applying a full-document seed (e.g., when creating a new note)
import { seedFromText } from "@agent-native/core/collab/ydoc-manager";

await seedFromText("note-123", "Hello world!", "content");

```

## Summary

- **Agent-Native Yjs CRDT real-time collaboration** relies on a server-side LRU cache capped at 50 documents with atomic write locks to prevent race conditions.
- Updates are debounced by 80ms on the client and coalesced using `Y.mergeUpdates` before transmission.
- The system uses a hybrid sync mechanism with SSE for low-latency updates and polling as a fallback, with automatic reconciliation when buffers overflow.
- External snapshots (like AI-generated content) use deterministic leader election based on Yjs `clientID` to avoid merge conflicts.
- All document states are persisted to a SQL `_collab_docs` table with automatic compaction when storage grows beyond 4× the optimal size.

## Frequently Asked Questions

### How does Agent-Native prevent conflicts when multiple users edit simultaneously?

Agent-Native relies on Yjs’s built-in CRDT properties to ensure conflict-free convergence. On the server, `withDocWriteLock` in [`packages/core/src/collab/ydoc-manager.ts`](https://github.com/BuilderIO/agent-native/blob/main/packages/core/src/collab/ydoc-manager.ts) serializes concurrent writes to the same document, while the CRDT itself handles concurrent edits through its immutable update mechanism. When external snapshots (like AI outputs) must be merged, the `isReconcileLeadClient` logic elects a single client with the lowest `clientID` to perform the reconciliation, ensuring deterministic results.

### What happens when a client goes offline or a tab loses visibility?

The `useCollaborativeDoc` hook implements visibility-aware polling. When a tab is hidden, it pauses polling and sets `visible:false` in its awareness state. Updates are buffered locally and flushed immediately when the tab becomes visible again or before the next poll cycle. If the client misses updates due to the server’s ring-buffer overflowing, it automatically detects the gap and requests a full state-vector update to catch up.

### How does the system handle AI agents differently from human users?

According to the source code in [`packages/core/src/collab/client.ts`](https://github.com/BuilderIO/agent-native/blob/main/packages/core/src/collab/client.ts), AI agents are assigned a `clientID` of `Number.MAX_SAFE_INTEGER`. Since the leader election algorithm selects the client with the lowest ID for merging external snapshots, this ensures the agent never becomes the reconcile lead. This design prevents AI-generated content from conflicting with active human editing sessions while still allowing the agent’s contributions to be integrated once a human client assumes leadership.

### What is the storage overhead of the Yjs document persistence?

The system implements automatic compaction in [`ydoc-manager.ts`](https://github.com/BuilderIO/agent-native/blob/main/ydoc-manager.ts). When the persisted blob grows to more than 4× the size of a fresh encoding, the system strips tombstones and optimizes the document. This keeps storage bounded while maintaining the full edit history required for CRDT convergence. The LRU cache on the server further reduces database load by keeping hot documents in memory.