Collaborating
Enter a display name in the 👥 Share tool rail and press "Join", and you share viewpoint, selection, and non-destructive edits in real time with everyone else in the same room.
Inviting someone
The room ID defaults to a UUID (an unguessable random value). Share it via the panel's "Invite link" button, or oniyanma.inviteLink() from the console.
https://demo.oniyanma.jp/?collab=wss://…&room=<uuid>&name=<you>Anyone who opens this link automatically joins the same room. The ID stays in localStorage, so a reload returns you to the same room.
There's no authentication yet
The server has no authentication — anyone who knows the room ID can join the room. That's exactly why the default ID is an unguessable value, and sharing is funneled through the invite link alone. "The default room can't be guessed" is, as it stands, the whole of access control (if this property broke, the screen would still look normal and someone could join silently, so the room-ID logic is pinned by regression tests). Identity and permissions are planned to be layered on once the requirements are settled.
What gets shared
Viewpoint. Other participants' cameras are shown as view frustums. Clicking a name in the presence list follows that person's viewpoint (operating the camera yourself releases the follow).
Selection. "Look here" can be handed over directly. What's shared is a predicate, not a list of points, so pressing "◻ Selection: n points" in the presence list means exactly the same points get selected on your own screen too.
If the coordinate frames are known on both sides and don't match, no box is drawn and the reason is shown instead ("◻ Has a selection (can't display — coordinate frames differ)"). Silently drawing a misaligned box would be worse.
Non-destructive edits. The edit log syncs over a Yjs shared array. Undo / Redo / Clear propagate too.
Findings. Findings and type definitions sync too, but in a separate room (<room>:entries) from the edit log, with a provider attached to a Doc the app owns — so findings stay with you after you leave. Ones filed before joining merge in on connect. → Sharing while collaborating
Per-user Undo
⌘Z only undoes the edits you made yourself. Other people's edits stay. Only your one entry, wherever it sits in the log, is removed, and Redo restores it to its original position, so later-wins relationships aren't disturbed either.
"Clear edits" is the one that clears everyone's, so a confirmation with the count shows up while collaborating.
The sync server
The production sync server is Cloudflare Durable Objects (packages/collab). Room = a DO instance (idFromName), so room isolation and connection management belong to the runtime.
The WebSocket entry point is a Pages Function, which forwards to the DO. This is a latency choice — the free-tier custom domain and the per-account *.workers.dev route to a US-west-coast PoP (measured min 103–138 ms), while *.pages.dev responds from Tokyo (measured min 9.5 ms / colo=NRT). Since the WS endpoint can live on a different host from the app's own origin, sync alone is routed to wss://<pages>/collab.
The protocol implementation (src/room.ts) is runtime-independent and shares the exact same code with the Node version used for local development (packages/server/src/collab.ts). A dual implementation would risk "works locally but breaks in production".
Running it locally:
pnpm dev:all # app + relay + collaboration (Node version, ws://8788)
pnpm collab:dev # the Durable Objects version (wrangler dev — same implementation as production)
pnpm collab:smoke # a wire-level connectivity test for the WorkerTo specify a connection explicitly, use the panel's "Advanced settings → Sync server" or ?collab=. To bake it in at build time, use VITE_COLLAB_URL.
Nothing is persisted
The server persists nothing. Once everyone leaves, the room is discarded. Edits autosave locally, so they stay on your machine, but to keep them as a deliverable, save them to a sidecar.
From the console
oniyanma.joinRoom('wss://…', '<room-id>', 'Display name')
oniyanma.inviteLink() // returns the invite URL
oniyanma.showPeerMarkers(false) // hide peers' viewpoint/selection markers
oniyanma.leaveRoom()