Collaboration & Review plugins
Six plugins that turn the editor into a real-world document workflow: slash commands, @mentions, Track Changes, threaded comments, revision history, and Yjs-backed presence. All ship in the standard license — no premium SKU required.
Slash commands
Type / on an empty line (or after whitespace) to open an inline command picker. 15 built-in blocks (H1–H3, bullet / numbered lists, quote, code, divider, table, image, link, emoji, today’s date) plus every registered AI Toolkit action.
Config
{
slashCommandEnabled: true, // default when this plugin is loaded
slashCommandIncludeAi: true // pull aiToolkitActions into the menu
}API
editor.slashCommands.register({ id, label, keywords, run });
editor.slashCommands.remove(id);
editor.slashCommands.list();
editor.slashCommands.open(); / .close();@Mentions
Trigger-character framework for @, #, [[wiki]], or any custom prefix. Async data sources (debounced, with a loading state). Atomic non-editable pills — backspace removes the whole chip.
Register a trigger
editor.mentions.register({
trigger: "@",
insertClass: "rte-mention-person",
search: function (query, done) {
fetch("/api/users?q=" + encodeURIComponent(query))
.then(r => r.json())
.then(done); // Array<{ id, name, color? }>
},
renderItem: function (user) { return user.name; },
renderInsert: function (user) { return "@" + user.name; }
});Track Changes (human suggesting mode)
Word-style suggesting. Insertions appear underlined in the current user’s color; deletions get a strikethrough (not removed until accepted). Sequential deletes merge into one span; backspacing inside your own just-typed insert shrinks it.
Config
{
trackChangesEnabled: true,
currentUser: { id: "maya", name: "Maya Patel", color: "#9333ea" },
reviewCanDecide: (entry, action, user) => user.role === "editor"
}Per-author accept / reject from the Review drawer. Use reviewCanDecide to enforce your application's reviewer policy; denied decisions leave the suggestion pending and raise review_decision_denied. Accepted and rejected suggestions store decidedBy for an audit trail. Works alongside AI suggestions — both end up in the same editor.reviewLedger store.
Revision history
Snapshot browser with a sandboxed preview iframe and an LCS-based line diff. Manual or debounced auto-snapshots. localStorage by default, or POST to a server endpoint.
Config
{
revisionHistoryEnabled: true,
revisionHistoryMaxEntries: 50,
revisionHistoryAutoSnapshotMs: 10000,
revisionHistoryUrl: "/api/revisions", // optional
revisionHistoryRequestHeaders: () => ({
"X-CSRF-Token": readCsrfToken()
}),
onRevisionHistorySyncError: ({ reason, status }) => {
reportSyncProblem(reason, status);
}
}Use a same-origin endpoint with your existing session and CSRF protection. Header values may be supplied by a function so a short-lived anti-forgery token can be read at request time. Do not place long-lived API keys in editor configuration.
API
editor.revisionHistory.snapshot(label?, metadata?);
editor.revisionHistory.promptAndSnapshot(defaultLabel?); // native prompt + snapshot
editor.revisionHistory.rename(id, label);
editor.revisionHistory.list(); // all snapshots
editor.revisionHistory.listNamed(); // user-labeled only
editor.revisionHistory.diff(idA, idB); // { lines, oldText, newText }
editor.revisionHistory.restore(id);
editor.revisionHistory.delete(id);
editor.revisionHistory.openBrowser();Yjs presence & concurrent typing
Live cursors, presence avatars, shared review ledger. Yjs is a peer dependency — the plugin never ships it. You load Yjs + a provider (WebSocket / WebRTC / Hocuspocus) and pass them in.
Attach
import * as Y from "yjs";
import { WebsocketProvider } from "y-websocket";
const ydoc = new Y.Doc();
const provider = new WebsocketProvider("wss://collab.example.com", docId, ydoc);
editor.collab.attach({
doc: ydoc,
provider: provider,
user: { id: "alice", name: "Alice", color: "#2563eb" },
textSync: "crdt", // per-node Yjs CRDT; load crdt-engine.min.js first
requireCrdt: true // do not silently fall back to legacy snapshots
});textSync: "crdt". The per-node binding mirrors the editable DOM into a Y.XmlFragment, preserving concurrent text and formatting changes without the legacy whole-body reset. Call editor.collab.getStatus() after attach to record the active mode and any fallback reason.
Production WebSocket edge
The included collaboration server supports signed, room-scoped tokens, durable LevelDB state, browser-origin allowlisting, and a per-room connection ceiling. Use all four controls in production; terminate TLS at your proxy and do not expose an open WebSocket endpoint.
RTE_COLLAB_SECRET="use-a-long-random-secret"
RTE_COLLAB_PERSIST="/var/lib/rte-collab"
RTE_COLLAB_ALLOWED_ORIGINS="https://app.example.com,https://admin.example.com"
RTE_COLLAB_MAX_CONNECTIONS_PER_ROOM="100"
RTE_COLLAB_REDIS_URL="redis://redis:6379"
node server.jsThe cloud token endpoint verifies document ownership before minting a short-lived credential for one room. The server rejects missing or unlisted browser origins with HTTP 403 and rejects excess peers in one hot room with HTTP 429. Set the same Redis URL on every replica to relay document and awareness updates without sticky sessions; a replica joining an active room requests a full CRDT state snapshot before syncing its first client. If configured Redis is unavailable, /healthz returns HTTP 503 and status: "degraded". After reconnect, readiness stays degraded until an internal Pub/Sub round-trip proves the subscription is receiving again.
Every peer is an untrusted input
A CRDT applies remote edits by writing nodes straight into the live document. It does not pass through paste handling or setHTMLCode, so an editor that filters only those two entry points will happily execute a collaborator’s <iframe srcdoc> in every other participant’s browser — and then replicate it onward. Treat a co-author on a shared document exactly as you treat pasted HTML.
sanitizer.js closes that path: it watches the editable for inserted nodes and attributes and filters anything executable before it can run, whatever produced it. Ordinary typing costs nothing because text nodes are skipped and the filter only engages when something dangerous appears. Disable with sanitizerGuardLiveDom: false if your sync layer already sanitizes upstream. You can run this check yourself on the live verification page.
The release suite starts two independent WebSocket server processes against an isolated real Redis-compatible process and verifies late join, bidirectional content and presence, room/token isolation, backend loss, readiness recovery, and the first edit after recovery. This is vendor-run integration evidence, not a managed-service SLA or an external reliability certification.
Shared review ledger
AI suggestions, human tracked-change suggestions, and comments all funnel through a single store: editor.reviewLedger. That’s how the Review drawer can show them interleaved and accept / reject works the same way regardless of source.
API
editor.reviewLedger.add(entry); // { id, changeType, author, text, replies[], ... }
editor.reviewLedger.update(id, patch);
editor.reviewLedger.remove(id);
editor.reviewLedger.get(id);
editor.reviewLedger.list();
editor.reviewLedger.refreshPanel();
Threaded comments
Comments anchor to selection ranges and render as a highlight + sidebar thread. Composer, replies, resolve, delete.
Config
API