Real-time collaboration
Apollon's collaboration layer is built on Yjs. Set collaborationEnabled: true in the constructor options, then wire your transport. The editor doesn't care whether messages travel over WebSocket, WebRTC, BroadcastChannel, or anything else — it hands you opaque base64 strings to ship and accepts them back.
const editor = new ApollonEditor(container, {
type: UMLDiagramType.ClassDiagram,
collaborationEnabled: true,
})
// Outbound: the editor calls your callback when it has bytes to send.
editor.sendBroadcastMessage((base64) => transport.send(base64))
// Inbound: forward every received frame back to the editor.
transport.onMessage((base64) => editor.receiveBroadcastedMessage(base64))
A complete transport (BroadcastChannel)
A full, working transport using the browser's BroadcastChannel — two tabs of
the same origin edit one diagram live. Swap BroadcastChannel for your
WebSocket/WebRTC channel; the editor calls are identical.
import { ApollonEditor } from "@tumaet/apollon"
export function connectCollaboration(editor: ApollonEditor, room = "apollon") {
const channel = new BroadcastChannel(room)
// Register the outbound sink before anything else — broadcasts are no-ops until set.
editor.sendBroadcastMessage((base64) => channel.postMessage(base64))
// Forward every inbound frame to the editor; it demuxes by message type.
channel.onmessage = (e: MessageEvent<string>) =>
editor.receiveBroadcastedMessage(e.data)
// On (re)connect: pull peers' document + presence, then push our own.
channel.postMessage(ApollonEditor.generateInitialSyncMessage())
channel.postMessage(ApollonEditor.generateInitialAwarenessSyncMessage())
editor.broadcastFullState()
return () => channel.close()
}
Who's online
subscribeToCollaboratorChanges fires with the current collaborators — each a
CollaboratorInfo ({ id, name, color, imageUrl?, clientIds, isLocal }):
import { ApollonEditor, type CollaboratorInfo } from "@tumaet/apollon"
function watchCollaborators(editor: ApollonEditor) {
const subId = editor.subscribeToCollaboratorChanges(
(collaborators: CollaboratorInfo[]) => {
const remote = collaborators.filter((c) => !c.isLocal)
console.log(`${remote.length} other editor(s) online`)
}
)
return () => editor.unsubscribe(subId)
}
Backing transports
Any Yjs-compatible transport works. The standalone server uses a custom WebSocket relay (see standalone/server/src/ws.ts); other deployments commonly use:
y-websocketfor self-hosted WebSocket relaysy-webrtcfor peer-to-peery-indexeddbfor offline persistence (layered alongside any other transport)- Any HTTP/3 stream or BroadcastChannel if your room is browser-local
Awareness (cursors, selections, follow)
Awareness state — who's online, where their cursor is, what they have selected, and where their viewport sits — rides on the same channel as document updates. The editor manages awareness internally; you don't need to wire anything beyond sendBroadcastMessage / receiveBroadcastedMessage.
The presence bar, cursors, selection highlights, and viewport-following are toggled per-feature on the collaboration option:
const editor = new ApollonEditor(container, {
collaboration: {
enabled: true,
user: { name: "Ada", color: "#1c7ed6" },
showPresence: true, // avatar bar (top-right)
showCursors: true, // live remote cursors
showSelectionHighlights: true, // highlight peers' selected elements
showFollow: true, // click a peer's avatar to mirror their viewport
},
})
When showFollow is on, clicking a collaborator's avatar follows their viewport. The follower sees the editor framed in that person's color and a banner naming them with a Stop button; the followed user sees a "followed by N" badge. Any local pan/zoom hands control back and stops following.
Server-side integration
If you want to drive the wire protocol from a Node server (integration tests, headless conversion, replication), use the unstable @tumaet/apollon/internals subpath:
import {
createHeadlessSync,
MessageType,
type YjsSync,
} from "@tumaet/apollon/internals"
/internals is explicitly not covered by SemVer. The standalone server's integration tests pin against it.