Signal drop!
Relay (operand.online) is unreachable.
Usually, a dropped signal means an upgrade is happening. Hold on!
Sorry, no connección.
Hang in there while we get back on track
gram: docs
> ./examples/collaboration/README.md
# docx-editor — Realtime Collaboration
A minimal demo of multi-user collaborative editing on top of `@eigenpal/docx-editor-react` using [Yjs](https://yjs.dev), [`y-prosemirror`](https://github.com/yjs/y-prosemirror), and [`y-webrtc`](https://github.com/yjs/y-webrtc).
**No backend required.** Peers find each other through Yjs's public WebRTC signaling servers and sync directly browser-to-browser.
## Try it
```bash
bun install
bun run dev
Open http://localhost:5273, then click Share link and paste the URL into a second browser window. Type in either window — edits, selections, and avatars sync live.
How it works
Four pieces:
externalContentprop tells the editor to treat itsdocumentprop as a schema seed only and skip the mount-time content load.ySyncPluginpopulates ProseMirror from the sharedY.Docinstead.externalPluginsreceivesySyncPlugin,yCursorPlugin(awareness), andyUndoPlugin()so Yjs owns the document state, remote cursors, and history. Tracked changes sync automatically through this —insertion/deletionmark attrs (author, date, revision id) ride along with the synced PM tree. Remote cursors and selection-range highlights also surface automatically via the editor's PM-decoration forwarding layer.- Awareness (Yjs's ephemeral state channel) carries each user's name, color, and selection. The
AvatarStackin the title bar readsprovider.awareness.getStates()and renders connected users. - Controlled
commentsprop + aY.Array<Comment>on the sameY.Doc. PM only carries the comment range markers; the thread metadata (text, author, replies, resolved status) lives in the Y.Array, mirrored into React state and pushed back throughonCommentsChange.
<DocxEditor
document={createEmptyDocument()} // schema seed only
externalContent // skip the load — Yjs owns content
externalPlugins={[ySync, yCursor, yUndo]}
comments={comments} // mirrored from Y.Array<Comment>
onCommentsChange={setComments} // writes back into Y.Array
author={user.name} // attribution for comments / track changes
renderTitleBarRight={() => <AvatarStack users={users} />}
/>
Files
| File | What it does |
|---|---|
src/App.tsx |
Wires identity, room, collaboration hook, and renders DocxEditor |
src/useCollaboration.ts |
Sets up Y.Doc, WebrtcProvider, awareness, the y-prosemirror plugins, and the comments Y.Array |
src/AvatarStack.tsx |
Overlapping circular avatars in the title bar |
src/identity.ts |
Per-tab user identity (sessionStorage) and room id from URL hash |
Caveats
This is a demo, not a production-ready collab template. Known gaps:
- Comments can lose data on concurrent edits. The Y.Array sync is naive replace-all (
delete(0, length); push(next)inside a transact). If two peers add a comment in the same instant, whichever transact lands second wipes the other's additions. For production, useY.Map<id, Comment>keyed by comment id — single-key writes resolve concurrent edits cleanly. - Comment IDs can collide between peers. Comment IDs come from a module-level scalar starting at 1, and the demo seeds with an empty document so the load-time bump never fires. First comment from each peer gets
id: 1. Tracked by #257. - Tracked-change accept/reject races. Two peers accepting/rejecting the same change at the same instant produce two PM transactions over overlapping ranges. Yjs picks an ordering and the loser's intent is silently dropped — no conflict UI.
- The public WebRTC signaling servers are best-effort. For a stable connection, deploy
y-websocket, PartyKit, Liveblocks, or Hocuspocus. - Sessions are ephemeral. Refresh in an empty room → the document disappears. Add
y-indexeddbfor local persistence, or a server-side persistence layer for shared persistence. - Loading an existing
.docxinto a live room is non-trivial. The source-of-truth swap fromdocument/documentBufferto theY.Docneeds to happen exactly once and only on a designated peer. Out of scope for this demo.