Real-Time Collaboration
Multi-user collaborative editing where the AI agent and human users are equal participants — like Google Docs, but with an AI collaborator.
Overview
The framework provides a Yjs-based collaborative editing system in @agent-native/core/collab. Multiple users can edit the same document simultaneously with live cursor positions, and the AI agent can make surgical edits that appear in real-time without disrupting the user's cursor, selection, or undo history.
This is built on three battle-tested technologies: Yjs (CRDT for conflict-free merging), TipTap (rich text editor), and polling-based sync (works in all deployment environments including serverless and edge).
How it works
The collaboration system has three layers:
- Yjs Y.Doc — stores the document as a
Y.XmlFragment(ProseMirror node tree). This is the CRDT that enables conflict-free merging of concurrent edits. - TipTap Collaboration extension — binds the editor to the Y.XmlFragment via
ySyncPlugin. Remote changes are applied as minimal ProseMirror transactions that preserve cursor position. - Polling sync — clients poll
/_agent-native/pollevery 2 seconds for Yjs updates. Awareness state (cursor positions, user info) is synced via a separate/_agent-native/collab/:docId/awarenessendpoint.
The Yjs state is persisted in a _collab_docs SQL table as base64-encoded binary, compatible with both SQLite and Postgres.
Agent + human collaboration
The agent and human users are equal participants in collaborative editing. The key insight is that both produce Yjs operations that merge cleanly:
- Human edits flow through TipTap → ySyncPlugin → Y.XmlFragment → server via HTTP
- Agent edits flow through the
edit-documentaction → server search-replace endpoint → Y.XmlFragment mutation → poll update → all clients
The agent's edit-document action uses surgical search-and-replace on Y.XmlText nodes within the Y.XmlFragment tree. This produces the smallest possible Yjs update — only the changed text is modified, not the entire document. The result: the user sees the agent's change appear in their editor without losing their place.
# Agent makes a surgical edit — user sees it appear live
pnpm action edit-document --id doc123 --find "Big Projects" --replace "Proyectos Grandes"
# The action:
# 1. Updates SQL content column (for search/API compat)
# 2. Calls POST /_agent-native/collab/doc123/search-replace
# 3. Server walks Y.XmlFragment, finds text, modifies Y.XmlText node
# 4. Minimal Yjs update emitted via poll system
# 5. Client receives update → ySyncPlugin applies targeted PM transaction
# 6. User's cursor stays in place ✓
Enabling collaboration
Templates opt into collaboration with five steps:
1. Install dependencies
pnpm add @tiptap/extension-collaboration @tiptap/extension-collaboration-caret @tiptap/y-tiptap @tiptap/core
2. Add Vite optimizeDeps
Prevents Vite from re-bundling TipTap in incompatible ways during dev:
// vite.config.ts
export default defineConfig({
plugins: [reactRouter()],
optimizeDeps: {
include: [
"yjs",
"y-protocols/awareness",
"@tiptap/core",
"@tiptap/extension-collaboration",
"@tiptap/extension-collaboration-caret",
"@tiptap/y-tiptap",
],
},
});
3. Add the collab server plugin
// server/plugins/collab.ts
import { createCollabPlugin } from "@agent-native/core/server";
export default createCollabPlugin({
table: "documents",
contentColumn: "content",
idColumn: "id",
autoSeed: false, // Client-side seeding on first load
});
4. Use the client hook
import { useCollaborativeDoc, generateTabId } from "@agent-native/core/client";
const TAB_ID = generateTabId();
const { ydoc, awareness, isLoading, activeUsers } = useCollaborativeDoc({
docId: documentId,
requestSource: TAB_ID,
user: { name: "Steve", email: "[email protected]", color: "#60a5fa" },
});
5. Add TipTap extensions
import Collaboration from "@tiptap/extension-collaboration";
import CollaborationCaret from "@tiptap/extension-collaboration-caret";
import { Awareness } from "y-protocols/awareness";
// Create awareness for cursor sync
const awareness = new Awareness(ydoc);
awareness.setLocalStateField("user", { name, color });
const editor = useEditor({
extensions: [
StarterKit.configure({ history: false }), // Yjs handles undo
Collaboration.configure({ document: ydoc }),
CollaborationCaret.configure({
provider: { awareness },
user: { name, color },
}),
],
content: initialContent,
});
Live cursors & presence
The CollaborationCaret extension renders colored cursor lines with user name labels for each connected user. The useCollaborativeDoc hook provides an activeUsers array that can be used to render a presence bar with user avatars.
User identity is derived from the session email. The framework provides emailToColor() and emailToName() helpers to generate consistent cursor colors and display names from email addresses.
Comments
Templates can add a comments system with threaded discussions on documents. The content template includes a full implementation with:
document_commentsSQL table (threads, replies, resolved status)- CRUD API routes at
/api/comments - Comments sidebar with threaded view and reply UI
- Resolve/unresolve threads
- Send to AI button — sends the comment thread context to the agent chat via
sendToAgentChat() - Agent actions:
list-comments,add-comment - Notion comment sync:
sync-notion-commentsaction for bidirectional pull/push
Collab routes
All collab routes are auto-mounted under /_agent-native/collab/ by the collab plugin:
| Route | Purpose |
|---|---|
GET /:docId/state |
Fetch full Y.Doc state (base64) |
POST /:docId/update |
Apply client Yjs update |
POST /:docId/text |
Apply full text replacement (diff-based) |
POST /:docId/search-replace |
Surgical find/replace in Y.XmlFragment |
POST /:docId/awareness |
Sync cursor/presence state |
GET /:docId/users |
List active users on a document |
Agent edit action
The edit-document action is the primary way agents make changes to documents in collaborative mode:
# Single edit
pnpm action edit-document --id doc123 --find "old text" --replace "new text"
# Batch edits
pnpm action edit-document --id doc123 --edits '[{"find":"old","replace":"new"}]'
# Delete text
pnpm action edit-document --id doc123 --find "delete me" --replace ""
When collab state exists for the document, the action calls the server's search-replace endpoint via HTTP (not the collab module directly, since actions run in a separate process). The server walks the Y.XmlFragment tree, finds the text in Y.XmlText nodes, and applies minimal delete/insert operations. The resulting Yjs update is broadcast to all connected clients via the poll system.
Common pitfalls
- TipTap version mismatch — All
@tiptap/*packages must be the same version. The Collaboration extension requireseditor.utilswhich was added in v3.22.2. Add@tiptap/coreas an explicit dependency. - Empty editor on first load — The Collaboration extension does NOT auto-seed from the
contentprop. Seed manually witheditor.commands.setContent()when the Y.XmlFragment is empty. - Data loss from empty saves — Guard against saving empty content in the
onUpdatehandler when the editor is in collab mode but hasn't been seeded yet. - Vite dep optimization — Always add Yjs-related packages to
optimizeDeps.includeto prevent Vite from re-bundling TipTap in incompatible ways. - Separate process for actions — Actions run via
pnpm actionin a new Node.js process. Use the server's HTTP endpoints (not the collab module directly) so updates reach the poll system.