Skip to main content
Every document is a Yjs CRDT from the start. A single-user editor is a room with one participant. This gives you:
  • Real-time editing over your own transport. Carets, presence, tracked changes, and comments are collaborative by default. Your authentication and infrastructure carry the traffic.
  • Word round trip after concurrent edits. Edits merge as CRDT operations, and the result exports to .docx with native tracked changes and comments.
  • A server in the same room. @reviseio/editor/server runs the same converters and document tools in Node. Your backend can create documents before anyone opens them, edit live rooms without a browser, and propose tracked changes for users to review.
The SDK never opens a socket. You own the transport, its authentication, and its lifecycle. Pass the provider to make the document collaborative:
The editor edits the provider’s document in place. The provider’s awareness carries carets and presence. The provider object marks which edits came from other people.

One copy of Yjs

Yjs identifies its own types with instanceof, so your application and the SDK must resolve to the same Yjs module. Yjs is a peer dependency. Install it yourself:
If the bundler gives each side a different copy, seeding a shared document fails with Unexpected content type in insert operation. In Vite, keep the SDK and Yjs on the same side of the dependency optimizer:
vite.config.ts
Webpack and Next.js resolve a single copy when your lockfile has only one yjs. Run npm ls yjs to check.

Connect callback

The editor reserves eight top-level keys in the Yjs document (content, chrome, notes, comments, annotations, metadata, sdt-wrappers, paragraphs) and needs specific document settings. The recommended form gives you a document and an awareness instance to connect:
Return the provider. The editor uses it to recognize edits from peers. Your application still owns the connection, its authentication, and its lifetime.

Supplying a Y.Doc

Some transports create the document themselves. Both of these forms work:
The editor checks a supplied document before writing to it: A refused document reports the conflicting key:
This happens if your application keeps one Yjs document per room for all of its data and already uses a name such as comments or metadata. Give the editor its own document, or keep your data in a separate one. Use the explicit form for an unrecognized transport, a custom sync layer, or a setup where the document and the connection are separate objects:
The explicit form requires remoteOrigin: the transaction origin your transport passes to Y.applyUpdate. Without it, the editor treats a peer’s edit as local. Cursor restoration breaks, and Ctrl+Z can undo someone else’s typing. A transport that applies updates with a null origin must be wrapped to tag its own transactions. Passing provider handles this, since most providers pass themselves as the origin.

Seeding

The first participant writes the content. Everyone else joins it. seed controls this:
Seeding happens after the first sync. Seeding before receiving the room’s state would duplicate the document.

Sync state

Set synced to true once the provider has received the room’s initial state. Until then, the editor keeps the document closed instead of showing an empty, editable page:
Later disconnections do not close the document. Yjs merges offline edits when the transport reconnects. A dropped connection affects presence only. If your transport has no sync signal, omit synced. The editor opens immediately, and a brand-new room can be seeded twice.

Roster

collaboration.getState() returns awareness as a list of peers, with the same colors the carets use:
It fires on join, leave, caret movement, and sync state changes. A participant with no currentUser does not appear.

Presence and cursors

Remote carets render when awareness is available, with a label and color per participant. Peers see what you pass as currentUser:
name labels the caret. image is drawn on its flag and can be any URL the browser can load, including a data: URI. color is optional. Without it, the editor derives a stable color from the user id, so each person has the same color for everyone. Set it to use your own palette or to match an avatar color. A participant with no currentUser has no caret for others to see. Pass one for anyone who should be visible.
currentUser also sets the author of tracked changes. Without it, suggestions export to Word with the reviewer name “Anonymous”.

Server editing

With @reviseio/editor/server, your server is a peer in the room, not only a relay. The backend entry runs the same converters and mutation engine as the editor. Changes your server makes to the Y.Doc reach every open browser as ordinary updates, and browser changes reach your server the same way. The backend reference covers the full API. This section covers common workflows.

Seeding from your own server

Your server can create the room from a file it already has:
Clients then pass seed: "never" and no source file, and join the existing document. This avoids a race where two people open a new document at the same time and both import it. The @reviseio/editor/server converters parse XML and HTML with DOM APIs, so jsdom is a peer dependency of this entry point. For a worked example of a @reviseio/editor/server service editing the same live document as the browser, see reviseio/sdk-demo.

Editing a live room

To change a room on the server, bind a document session to it. The session has the same document tools as the browser: read, search, measure, mutate, and comment. Edits go through the same Yjs-backed mutation engine, so peers receive ordinary updates and remote carets stay in place:
Sessions default to suggesting mode. A service proposes tracked changes unless it opts into direct edits with mode: "editing" or a per-call directMode. Nothing an unattended backend job proposes is applied until a person accepts it.

Review across server and browser

Server and browser share the same tool API. tools.execute() returns the same ReviseToolResult, mutations report the same suggestionIds, and accept and reject return the same ReviseSuggestionDecision. The server can propose changes and a person can resolve them in the browser:
It also works in reverse. A person suggests changes in the browser, and a scheduled job reads session.listSuggestions(). Each pending record includes its author, so the job can resolve its own agent’s suggestions and leave human ones alone. See tracked changes for the review UI.

Working without a live transport

A server can participate without a socket. For an API endpoint or a queue worker, the client posts its document as one encoded update. The server edits a decoded copy and returns only the delta. The delta merges into the live document like any peer’s edit, even if the user kept typing in the meantime:
The server keeps no state and needs no transport. The response carries the created suggestionIds for the same review flow as above.

What the SDK does not do

  • It does not connect. No socket, retries, authentication, or reconnection backoff. Your provider handles all of it.
  • It does not persist. Your server decides where the document is stored between sessions.
  • It does not destroy your Y.Doc. A document you pass in outlives the component. Unmounting only clears this client’s caret.
  • It does not enforce who may edit. Roles limit what each embed can do, but each client sets its own role, and there is no document locking. To enforce a role, check it on the server at the transport. The backend entry lets that server process read and edit the document it guards.