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.sessionbefore 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:
seedFromJsoninjects 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
versioncolumn 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 aY.Docinstance that automatically subscribes toGET /collab/:docId/stateand pushes local changes viaPOST /collab/:docId/updateuseCollabAwareness: Manages cursor positions and user selections through awareness endpoints (GET /collab/:docId/usersandPOST /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
- The Agent-Native collab module in
BuilderIO/agent-nativeprovides a complete Yjs-based CRDT solution for real-time editing without WebSocket infrastructure - Server-side implementation in
packages/core/src/server/collab-plugin.tsexposes REST endpoints with session authentication and persists to the_collab_docstable viapackages/core/src/collab/storage.ts - Client hooks
useCollabanduseCollabAwarenessinpackages/core/src/client/collab/index.tshandle polling-based synchronization and cursor awareness - Document patterns follow
type:${id}format, with helpers likehasCollabState,pushCollabUpdate, andseedFromJsonmanaging the data flow between SQL and Yjs - Template examples in
templates/videos/app/hooks/use-composition-collab.tsdemonstrate production-ready integration patterns for React applications
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →