Collaboration
Folio collaborates the way ProseMirror does: a central authority orders steps. Clients send steps tagged with the version they're based on; the authority accepts only steps based on its current version; every client pulls the steps since its own version and rebases its unconfirmed ones on top (prosemirror-collab). Positions, carets and selection highlights are computed from Folio's layout, so remote cursors land exactly on the paginated text.
There are three layers, from smallest to most complete.
1. In-memory: LocalAuthority + attachCollab
Built into @nextgensoftwares/folio-editor. Two editors in one page share an authority, as two users would through a server. Steps cross the "wire" as JSON and are rebuilt with the authority's schema, exactly as they would be over a network.
import { attachCollab, collabPlugin, FolioEditor, LocalAuthority } from '@nextgensoftwares/folio-editor';
const writers = [
{ clientID: 'A', name: 'Writer A', color: '#e11d48' },
{ clientID: 'B', name: 'Writer B', color: '#2563eb' },
];
const editors = writers.map((w) => new FolioEditor({ schema, doc, layout, measurer, plugins: [collabPlugin(0, w.clientID)] }));
const authority = new LocalAuthority(editors[0]!.state.doc);
const detach = editors.map((e, i) => attachCollab(e, authority, writers[i]!));
// Presence: everyone's selection, for drawing remote carets.
const others = authority.presence().filter((p) => p.clientID !== 'A');
// <FolioView editor={editors[0]} remotes={others} />attachCollab sends local steps, applies remote ones (mapping the selection backward so your caret stays put), and publishes the selection as presence once in sync. A server implements the same CollabAuthority interface (receiveSteps, stepsSince, setPresence, presence, subscribe) and persists the document.
editor.presenceGeometry(people, window) turns collaborators' { anchor, head } into carets and highlight rects (positions are clamped to this document), and FolioView's remotes prop draws them with name labels.
2. Over HTTP: SyncClient
@nextgensoftwares/folio-sync's SyncClient uses the same step protocol over REST with polling: debounced batched pushes, pull + rebase on conflict, offline outbox, server-side checksums and the mass-delete guard. Two clients editing the same document through it converge; this is covered by packages/sync/src/sync.test.ts. It's the right choice for "Google Docs-style autosave" with occasional concurrent editors. See Loading & saving.
3. Realtime: @nextgensoftwares/folio-plugin-collab in progress
Realtime collaboration over WebSockets: a transport with reconnects and backoff, presence (carets, typing, idle, follow), conflict-safe step sync on @nextgensoftwares/folio-sync's server logic, version history, blame, roles, and an offline flush. The reference server (@nextgensoftwares/folio-server-node) hosts it at /docs/:id/collab: everyone who opens the same document on the same server joins its room. While a document is open live, the socket carries the saves and the REST SyncClient is not used. See the collaboration plugin page and Real networks.
Rules that keep collaboration safe
- Loading is not an edit. Content appended while a document streams in is marked as already confirmed for the collab plugin, so it is never sent back.
- Out-of-band changes are steps. If the server changes a document (replace a chapter, run a migration), it must do so as steps every client rebases on, never as a raw database overwrite.
- Declare every attribute. ProseMirror drops undeclared attributes when steps are rebuilt from JSON; plugins must declare what they store.
- Keep transient state out of the document. Upload progress, selections and hover state are presence or local stores, never steps.