Skip to main content

Collaboration โ€” Presence & Comments

Tags: Presence, Comments ยท Version: v1 ยท Stability: ๐ŸŸก Beta

Real-time-ish collaboration without full co-editing: see who's looking at a template, and discuss it inline with @mentions. Both are JWT-only and workspace-scoped.

Versioning

These endpoints are ๐ŸŸก Beta โ€” functionally complete and shipped end-to-end, but newer than the core surface and may still gain response fields within v1. Pin to the documented fields.

Presenceโ€‹

Presence is poll-based: clients send a heartbeat while viewing a resource and fetch the current viewers. The designer itself uses the WebSocket collab hub below; this HTTP presence API remains for surfaces without a live document (the templates list).

Send a heartbeatโ€‹

POST /api/presence ยท JWT

Request โ€” HeartbeatBody
{ "resourceType": "template", "resourceId": "3f6bโ€ฆuuid" }

Both fields required (resourceId is a UUID). Call periodically while a user views the resource to keep them shown as present.

Get current viewersโ€‹

GET /api/presence/{resourceType}/{resourceId} ยท JWT โ€” who is currently viewing this resource (e.g. "Sarah is viewing this template").

Clear presenceโ€‹

DELETE /api/presence/{resourceType}/{resourceId} ยท JWT โ€” explicitly mark the user as no longer viewing (e.g. on navigate-away).

{resourceType} is a string (e.g. template); {resourceId} is a UUID.


Commentsโ€‹

Threaded discussion attached to a template, with @mention resolution.

List commentsโ€‹

GET /api/templates/{templateId}/comments ยท JWT โ€” {templateId} is a UUID.

Add a commentโ€‹

POST /api/templates/{templateId}/comments ยท JWT

Request โ€” CreateCommentBody
{ "body": "Can we move the logo up? cc @sarah" }

body is required. @mentions are resolved to workspace members.

Delete a commentโ€‹

DELETE /api/templates/{templateId}/comments/{commentId} ยท JWT

{commentId} is a UUID. Allowed for the comment author or a workspace Owner โ€” otherwise 403 Forbidden.


Live collaboration (Yjs over SignalR) ๐ŸŸขโ€‹

Saved templates are edited as one shared CRDT document. The browser connects to /hubs/collab (SignalR, MessagePack protocol required, JWT passed as ?access_token=) and joins doc:{templateId}.

Hub methodDirectionPurpose
JoinDocument(docId, awarenessClientId)client โ†’ server, returns {snapshot, snapshotSeq, updates[], peerAwareness[], canEdit}Durable state to converge on; canEdit is false for Viewers
SendUpdate(docId, bytes) โ†’ seqclient โ†’ serverOne Yjs update; persisted before fan-out
ReceiveUpdate(seq, bytes)server โ†’ othersA peer's update
SendAwareness(docId, bytes) / ReceiveAwareness(bytes)bothCursors, selection, interaction lock, drag ghost (never persisted)
PeerLeft(clientId)server โ†’ othersA peer disconnected

Compaction rides the ordinary save: PUT /api/templates/{id} may carry yState (base64) and yStateSeq; the server keeps one compacted snapshot per template and drops covered log rows. Viewers may join and watch but cannot write.

Version history ๐ŸŸขโ€‹

MethodPathNotes
GET/api/templates/{id}/versions?limit=50&before=Newest first; kind is auto / manual / restore
GET/api/templates/{id}/versions/{versionId}Includes content
POST/api/templates/{id}/versions { label }Name the current saved state
PATCH/api/templates/{id}/versions/{versionId} { label }Rename

A version is recorded on every successful save (identical content is skipped, rapid saves by the same author coalesce for 2 minutes; auto versions are thinned to hourly after 24h and daily after 30 days, capped at 200). Restore happens in the designer as a forward edit into the live document followed by a save with versionKind: "restore", so every collaborator converges and the restore itself can be undone.