- Real-time multi-user editing over a transport you own — carets, presence, tracked changes, and comments are all collaborative out of the box, with your authentication and your infrastructure in the path, not ours.
- Word fidelity that survives collaboration — concurrent edits merge as
CRDT operations, and the result still round-trips to
.docxwith native tracked changes and comments intact. - A server that participates in the same room —
@reviseio/sdk/backendruns the same converters and the same semantic tools under Node, so your backend can create documents before anyone opens them, edit live rooms headlessly, and propose tracked changes your users review in the browser. No other DOCX SDK offers this half at all.
One copy of Yjs
Yjs identifies its own types withinstanceof, so your application and the
SDK must resolve to the same Yjs module. Install it yourself — it is a
peer dependency, not something we bundle:
Unexpected content type in insert operation. In Vite, keep the
SDK and Yjs on the same side of the dependency optimiser:
vite.config.ts
yjs in your lockfile; npm ls yjs is the check.
Let the editor build the document
The editor reserves eight top-level keys in the Yjs document —content,
chrome, notes, comments, annotations, metadata, sdt-wrappers,
paragraphs — and relies on specific document settings. So the recommended
form hands you a document and an awareness instance and asks you only to
connect them:
Bringing your own document
Some transports create the document themselves, so both of these still work:comments and metadata are names you may well
have used first. Give the editor its own document, or keep your data in a
separate one.
The explicit path is otherwise the escape hatch — for a transport we do not
recognise, a custom sync layer, or a setup where the document and the
connection are separate objects:
Who seeds the document
The first participant establishes the content; everyone else joins it. That is whatseed controls:
"if-empty"(default) — if the shared document has no content, thedocxsource is written into it. If it already has content, the source is ignored and you join what is there."never"— always join.docxbecomes unnecessary, and a document input with no source is valid:
Seeding happens after the first sync, never before. A participant who seeded
a room they had not yet received the state of would duplicate the entire
document.
Telling us when you are synced
synced is how you say the provider has received the room’s initial state.
Until it turns true the editor holds the document closed rather than showing an
empty page someone can type into:
synced and the editor opens
immediately — accepting the duplicate-seed risk on a brand-new room.
Who is in the room
collaboration.getState() normalises awareness into a roster, using the same
colours the carets are drawn in:
currentUser does
not appear, because there is nothing to show.
Presence and cursors
Remote carets render automatically when awareness is available, labelled and coloured per participant. Everything a peer sees comes fromcurrentUser:
name labels the caret and image is drawn on its flag — any URL the browser
can load, including a data: URI, so an app with no avatar CDN can still give
each participant a face.
color is optional. Without it, a stable colour is derived from the user id,
so the same person is the same colour for everyone with no coordination. Set it
when the colours are yours to choose — brand palettes, or matching an avatar
you already show elsewhere.
A participant with no currentUser has no caret drawn for others: there is
nothing to label it with. Pass one for anybody who should be visible.
currentUser is also what stamps authorship on tracked changes. Without it,
suggestions export to Word as reviewer “Anonymous”. Set it once and both
problems go away.Your server is a participant
Everything above treats the server as plumbing: it moves updates between browsers.@reviseio/sdk/backend makes it a peer. Because the room is a
Yjs document and the backend entry runs the same converters and mutation
engine as the editor, anything your server does to the Y.Doc reaches every
open browser as an ordinary update — and everything a browser does reaches
your server the same way. The full surface is in the
backend reference; this section is the workflows.
Seeding from your own server
The browser is not the only place that can create a document. Your server can turn a file your systems already hold into the room itself:seed: "never" and no source file: everyone joins a
document that already exists. This is the difference between a product where
the first browser to open a document imports it — a race when two people click
at once — and one where documents exist before anyone opens them.
jsdom is a peer dependency of that entry point; the converters parse XML and
HTML with DOM APIs. A full worked example, with a syncing and persisting
server, is in examples/collaborative-docx.
Editing the live room headlessly
To mutate a room on the server, bind a document session to it. The session exposes the same semantic tools the browser does — read, search, measure, mutate, comment — transacting through the same Yjs-backed mutation engine, so peers receive ordinary updates and remote carets never miss a beat:mode: "editing", or a per-call
directMode). That default is what makes an unattended backend workflow safe
to point at a shared document — nothing is applied until a person accepts it.
One review loop, both surfaces
Server and browser speak the same tool contract —tools.execute() returns
the same ReviseToolResult envelope, mutations report the same
suggestionIds, and accept/reject returns the same ReviseSuggestionDecision
on either side. So a review pipeline composes across the stack: the server
proposes, the person in the browser decides.
session.listSuggestions() — every pending record carries
authorship metadata — and settles its own agent’s proposals while leaving
human ones alone. See tracked changes for the
review UI half.
Working without a live transport
A server does not need a socket to participate. For request/response integrations — an API endpoint, a queue worker — the client posts its document as one encoded update, the server works on a decoded copy, and answers with only the delta — which merges into the live document like any peer’s edit, even if the user kept typing during the round-trip:suggestionIds.
What the SDK does not do
- It does not connect. No socket, no retries, no auth, no reconnection backoff. Your provider owns all of it.
- It does not persist. Where the document lives between sessions is your server’s business.
- It does not destroy your
Y.Doc. A document you passed in outlives the component; unmounting only clears this client’s caret. - It does not arbitrate who may edit. Per-participant roles constrain what each embed can do, but they are set by each client, and there is no document locking. If a role must be authoritative, your server has to enforce it on the transport — which the backend entry makes practical, since the enforcing process can read and edit the document it is guarding.