How to Use the Agent-Native Collab Module for Real-Time Features

The Agent-Native collab module provides a full-stack, CRDT-based real-time collaboration layer using Yjs that enables multi-user editing through React hooks, server-side persistence, and automatic conflict resolution without writing custom WebSocket code.

The BuilderIO/agent-native repository ships with a production-ready collaboration system built on Yjs CRDTs. The Agent-Native collab module handles everything from client-side state synchronization to server-side persistence in the _collab_docs SQL table, allowing developers to add real-time features to any JSON-serializable resource such as slides, videos, or plans.

Architecture of the Agent-Native Collab Module

The collab module splits responsibilities across client and server layers, using Yjs for conflict-free replicated data type (CRDT) merging and optimistic concurrency control.

Server-Side Collab Plugin

The server implementation resides in packages/core/src/server/collab-plugin.ts. This Nitro plugin mounts REST endpoints under /_agent-native/collab/:docId/:action and handles authentication guards, persistence, and real-time broadcasting.

Key components include:

  • Route Guards: Every request validates event.context.session before processing, returning 401 for unauthenticated users
  • CollabEmitter: Broadcasts Yjs updates to connected polling clients via the ring-buffer implemented in packages/core/src/server/poll-events.ts
  • Body Size Limits: Write payloads are capped at 2MB by default to protect the database from oversized updates

The plugin supports actions including state, update, awareness, json, patch, and delete, routing each to the appropriate handler in the storage layer.

Storage and Y-Doc Management

Persistent storage is managed through packages/core/src/collab/storage.ts, which interacts with the _collab_docs table containing doc_id (primary key), yjs_state (base64-encoded BLOB), and version (INTEGER) columns.

The packages/core/src/collab/ydoc-manager.ts file handles document lifecycle operations:

  • Seeding: seedFromJson injects initial JSON into a fresh Y.Doc when creating new collaborative resources
  • Merging: Applies incoming patches atomically via setCollabState, ensuring idempotent writes
  • Versioning: Increments the version column on every successful write, enabling optimistic concurrency checks on the client

Client-Side Integration

The client layer in packages/core/src/client/collab/index.ts exposes React hooks that abstract the polling-based real-time connection:

  • useCollab({docId, initialState}): Returns a Y.Doc instance that automatically subscribes to GET /collab/:docId/state and pushes local changes via POST /collab/:docId/update
  • useCollabAwareness: Manages cursor positions and user selections through awareness endpoints (GET /collab/:docId/users and POST /collab/:docId/awareness)

The client automatically registers Tiptap collaboration extensions (@tiptap/extension-collaboration and @tiptap/extension-collaboration-caret) defined in packages/core/src/vite/client.ts, binding ProseMirror editors to the shared Y.Doc.

Implementing Real-Time Collaboration

Setting Up a Collab-Enabled Hook

Following the production pattern in templates/videos/app/hooks/use-composition-collab.ts, create a hook that seeds documents and handles two-way synchronization:

import { useCollab, useCollabAwareness } from '@agent-native/core/client/collab';
import { hasCollabState, pushCollabUpdate } from '@agent-native/core/collab';

export function useCompositionCollab(compositionId: string | null) {
  const docId = compositionId ? `composition:${compositionId}` : null;

  const { ydoc, ready, error } = useCollab({
    docId,
    seed: async () => {
      if (docId && !(await hasCollabState(docId))) {
        const json = await fetchCompositionJson(compositionId!);
        await pushCollabUpdate(docId, { type: 'seed', json });
      }
      return null;
    },
  });

  const { users, setAwareness } = useCollabAwareness({ docId });

  const composition = useMemo(() => {
    if (!ready) return null;
    return ydoc?.getMap('composition').toJSON();
  }, [ready, ydoc]);

  const save = async (newData: any) => {
    if (!docId) return;
    await pushCollabUpdate(docId, { type: 'patch', json: newData });
  };

  return { composition, save, users, setAwareness, ready, error };
}

This pattern checks for existing collab state with hasCollabState, seeds new documents with initial JSON when needed, and provides a save helper that pushes structured updates via pushCollabUpdate.

Syncing Server Actions with Collab State

Server actions must write through to the collab layer to maintain synchronization across all connected clients. Based on templates/videos/actions/update-composition.ts:

import { pushCollabUpdate } from '@agent-native/core/collab';
import { sql } from 'drizzle-orm';

export async function updateSlide({ slideId, changes }) {
  // Update the canonical database first
  await db.update('slides')
    .set({ slide_json: sql`json_set(slide_json, '$', ${JSON.stringify(changes)})` })
    .where(eq('slides.id', slideId));

  // Push the same change to the collab document
  const collabDocId = `slide:${slideId}`;
  await pushCollabUpdate(collabDocId, { type: 'patch', json: changes });
}

When deleting resources, call deleteCollabState (as demonstrated in templates/plan/actions/delete-visual-plan.ts) to clean up the corresponding _collab_docs row and prevent orphaned collaboration data.

Document Identification and Routing

The collab module uses structured docId patterns to identify collaborative resources:

  • Plan blocks: plan:${planId}:${blockId}
  • Compositions: composition:${id}
  • Slides: slide:${slideId}

The server extracts these IDs from the URL route /_agent-native/collab/:docId/:action defined in packages/core/src/server/collab-plugin.ts. The storage layer in packages/core/src/collab/storage.ts uses these IDs as primary keys in the _collab_docs table, with getCollabState retrieving current binary state and setCollabState performing atomic upserts.

Summary

Frequently Asked Questions

How does the Agent-Native collab module handle conflicts between simultaneous editors?

The module uses Yjs CRDTs (Conflict-free Replicated Data Types) implemented in packages/core/src/collab/ydoc-manager.ts to automatically merge concurrent changes. When multiple users push updates via pushCollabUpdate, the Y-Doc manager applies patches to the shared Y.Doc instance and persists the merged state with an incremented version number, ensuring all clients converge to the same document state without manual conflict resolution.

What database table stores the collaborative document state?

The _collab_docs table defined in packages/core/src/collab/storage.ts stores three columns: doc_id (primary key), yjs_state (base64-encoded binary Yjs document), and version (integer for optimistic concurrency). The setCollabState function performs atomic upserts to this table, while getCollabState retrieves the current binary state for client synchronization.

Do I need to manage WebSocket connections for real-time updates?

No. The Agent-Native collab module uses a polling-based architecture rather than WebSockets. The client hooks in packages/core/src/client/collab/index.ts poll the GET /collab/:docId/state endpoint, while the server broadcasts updates through the CollabEmitter ring-buffer in packages/core/src/server/poll-events.ts. This approach eliminates WebSocket infrastructure while maintaining sub-second synchronization for collaborative editing.

How do I initialize a new collaborative document for an existing resource?

Use the seedFromJson pattern demonstrated in templates/videos/app/hooks/use-composition-collab.ts. First check if a collab document exists using hasCollabState(docId). If not, fetch your resource's JSON data and call pushCollabUpdate(docId, { type: 'seed', json: initialData }). This creates the initial Yjs document in the _collab_docs table with version 1, making it available for real-time collaboration.

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 →