> ## Documentation Index
> Fetch the complete documentation index at: https://revise.io/developer/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Collaboration

> Real-time multi-user editing over your own transport, with your server as a participant.

Every document is a [Yjs](https://yjs.dev) 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:

```tsx theme={null}
import { HocuspocusProvider } from "@hocuspocus/provider";

const provider = new HocuspocusProvider({
  url: "wss://collab.example.com",
  name: "contract-1",
});

<ReviseEditor
  currentUser={{ id: user.id, name: user.name, image: user.avatarUrl }}
  initialDocuments={[
    {
      id: "contract-1",
      source: file,
      collaboration: { provider, synced },
    },
  ]}
/>;
```

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:

```bash theme={null}
npm install yjs y-protocols
```

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:

```ts vite.config.ts theme={null}
export default defineConfig({
  resolve: { dedupe: ["yjs", "y-protocols"] },
  optimizeDeps: { exclude: ["@reviseio/editor", "yjs", "y-protocols"] },
});
```

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:

```ts theme={null}
collaboration: {
  connect: (doc, awareness) =>
    new HocuspocusProvider({ url, name: "contract-1", document: doc, awareness }),
  synced,
}
```

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:

```ts theme={null}
collaboration: { provider }            // reads document/doc/getYDoc() + awareness
collaboration: { ydoc, awareness, remoteOrigin }
```

The editor checks a supplied document before writing to it:

| Document state | Result |
| - | - |
| Empty | Seeded |
| Holds a Revise document | Joined |
| Reserved keys hold other data | Refused |

A refused document reports the conflicting key:

```
Revise collaboration: this Y.Doc already uses "metadata", which the editor
reserves for its own document.
```

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:

```ts theme={null}
collaboration: {
  ydoc,
  awareness,                 // omit or pass null for no presence
  remoteOrigin: myTransport, // see below
  synced: isSynced,
  seed: "if-empty",
}
```

<Warning>
  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.
</Warning>

## Seeding

The first participant writes the content. Everyone else joins it. `seed`
controls this:

| Value | Behavior |
| - | - |
| `"if-empty"` (default) | If the shared document is empty, the `source` document is written into it. Otherwise the source is ignored and the editor joins the existing content. |
| `"never"` | Always join. `source` is not needed, and a document input with no source is valid. |

```ts theme={null}
{ id: "contract-1", collaboration: { provider, synced, seed: "never" } }
```

<Note>
  Seeding happens after the first sync. Seeding before receiving the room's
  state would duplicate the document.
</Note>

## 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:

```tsx theme={null}
const [synced, setSynced] = useState(false);

useEffect(() => {
  provider.on("synced", () => setSynced(true));
}, [provider]);
```

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:

```ts theme={null}
const { enabled, synced, peers } = editor.collaboration.getState();
// peers: [{ clientId, isLocal, id, name, email, image, color }]

useEffect(() => editor.collaboration.subscribe(setPresence), [editor]);
```

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`:

```tsx theme={null}
<ReviseEditor
  currentUser={{
    id: user.id,
    name: user.name,
    email: user.email,
    image: user.avatarUrl, // drawn on the caret flag
    color: user.brandColor, // caret, flag, and roster color
  }}
/>
```

`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.

<Note>
  `currentUser` also sets the author of tracked changes. Without it,
  suggestions export to Word with the reviewer name "Anonymous".
</Note>

## 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](/developer/docs/developer/docs/editor-sdk/api/backend) 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:

```ts theme={null}
import { seedYDocFromFile, encodeYDoc, ydocToDocx } from "@reviseio/editor/server";

// When a room is first opened, before any client finishes syncing:
await seedYDocFromFile(ydoc, docxBytes, "agreement.docx");

// Store the encoded bytes however you like:
await store.put(roomId, encodeYDoc(ydoc));

// Export the live room without a browser:
const bytes = await ydocToDocx(ydoc);
```

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](https://github.com/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:

```ts theme={null}
import { createServerDocumentSession } from "@reviseio/editor/server";

const session = await createServerDocumentSession(ydoc, {
  documentId: "agreement-1",
});
try {
  // Sessions default to suggesting, so this appears as a tracked change
  // in every open tab.
  const edit = await session.tools.call("replace", {
    search_result_id: searchResultId,
    replacements: [{ find: "Delaware", replace: "New York" }],
  });
  await notifyReviewers(edit.suggestionIds ?? []);
} finally {
  session.dispose(); // your Y.Doc stays alive
}
```

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:

```ts theme={null}
// Server: propose, and pass the IDs to your workflow.
const edit = await session.tools.call("replace", { ... });
await queueForReview(edit.suggestionIds ?? []);

// Browser: later, resolve that batch from your review UI.
const decision = editor.review.acceptSuggestions(await reviewBatch());
// { resolved, missing, unresolved }. Stale IDs are listed in `missing`.
```

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](/developer/docs/developer/docs/editor-sdk/guides/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:

```ts theme={null}
// Client
const response = await api.review({ update: Y.encodeStateAsUpdate(ydoc) });
Y.applyUpdate(ydoc, response.delta, "my-backend");

// Server
import { decodeYDoc } from "@reviseio/editor/server";

const ydoc = decodeYDoc(update);
const baseline = Y.encodeStateVector(ydoc);
// ...run a session, propose suggestions...
const delta = Y.encodeStateAsUpdate(ydoc, baseline);
```

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](/developer/docs/developer/docs/editor-sdk/guides/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](/developer/docs/developer/docs/editor-sdk/api/backend) lets that server
  process read and edit the document it guards.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.