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/poll every 2 seconds for Yjs updates. Awareness state (cursor positions, user info) is synced via a separate /_agent-native/collab/:docId/awareness endpoint.

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-document action → 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_comments SQL 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-comments action 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 requires editor.utils which was added in v3.22.2. Add @tiptap/core as an explicit dependency.
  • Empty editor on first load — The Collaboration extension does NOT auto-seed from the content prop. Seed manually with editor.commands.setContent() when the Y.XmlFragment is empty.
  • Data loss from empty saves — Guard against saving empty content in the onUpdate handler when the editor is in collab mode but hasn't been seeded yet.
  • Vite dep optimization — Always add Yjs-related packages to optimizeDeps.include to prevent Vite from re-bundling TipTap in incompatible ways.
  • Separate process for actions — Actions run via pnpm action in a new Node.js process. Use the server's HTTP endpoints (not the collab module directly) so updates reach the poll system.