Appearance
WebSocket Protocol
Reference for the Pulse real-time WebSocket protocol. Use this to build custom clients in any language.
Connection
Connect to ws://your-server:4567 (or wss:// for TLS) as a standard WebSocket. The server listens on the root path (/).
After the connection opens, immediately send an auth message. The server responds with auth:ok on success or auth:error on failure.
All messages are JSON-encoded strings.
Client → Server Messages
auth
Sent immediately after connection. Required before any other message.
json
{
"type": "auth",
"apiKey": "pk_your_publishable_key",
"token": "eyJhbGci...",
"room": "my-room",
"deviceType": "desktop"
}| Field | Type | Required | Description |
|---|---|---|---|
apiKey | string | Yes | Publishable key (pk_...) |
token | string | Yes | User JWT from /auth/token |
room | string | Yes | Room identifier |
deviceType | "desktop" | "mobile" | "tablet" | No | Device type for presence |
cursor:move
Send the current cursor position.
json
{
"type": "cursor:move",
"position": { "x": 0.5, "y": 0.3, "pageX": 500, "pageY": 300 }
}| Field | Type | Description |
|---|---|---|
position.x | number | Relative X position (0-1 range) |
position.y | number | Relative Y position (0-1 range) |
position.pageX | number | Absolute X position in pixels |
position.pageY | number | Absolute Y position in pixels |
presence:update
Update your presence status.
json
{ "type": "presence:update", "status": "idle" }| Field | Type | Description |
|---|---|---|
status | "online" | "idle" | Presence status |
thread:create
Create a new thread with an initial comment.
json
{
"type": "thread:create",
"id": "thread-uuid",
"body": "This button looks off",
"mentions": ["user-2"],
"position": { "x": 0.45, "y": 0.72 },
"attachmentIds": ["attach-uuid"]
}| Field | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Client-generated thread ID |
body | string | Yes | First comment text |
mentions | string[] | Yes | User IDs to @mention |
position | PinPosition | null | Yes | Pin position (see below), or null for unpinned |
attachmentIds | string[] | No | Pre-uploaded attachment IDs |
PinPosition fields:
| Field | Type | Required | Description |
|---|---|---|---|
x | number | Yes | Horizontal position (0-1 range) |
y | number | Yes | Vertical position (0-1 range) |
selector | string | No | CSS selector of the target element |
path | string | No | DOM path to the target element |
elementOffsetX | number | No | Horizontal offset within the target element |
elementOffsetY | number | No | Vertical offset within the target element |
scrollX | number | No | Page scroll X at time of pinning |
scrollY | number | No | Page scroll Y at time of pinning |
comment:create
Reply to an existing thread.
json
{
"type": "comment:create",
"threadId": "thread-uuid",
"id": "comment-uuid",
"body": "Agreed, fixing now",
"mentions": ["user-1"],
"attachmentIds": ["attach-uuid"]
}| Field | Type | Required | Description |
|---|---|---|---|
threadId | string | Yes | Thread to reply to |
id | string | Yes | Client-generated comment ID |
body | string | Yes | Comment text |
mentions | string[] | Yes | User IDs to @mention |
attachmentIds | string[] | No | Pre-uploaded attachment IDs |
comment:edit
Edit your own comment.
json
{
"type": "comment:edit",
"commentId": "comment-uuid",
"body": "Updated text",
"mentions": ["user-1"]
}| Field | Type | Description |
|---|---|---|
commentId | string | Comment to edit |
body | string | New comment text |
mentions | string[] | Updated mention list |
comment:delete
Delete your own comment.
json
{ "type": "comment:delete", "commentId": "comment-uuid" }thread:resolve
Resolve or reopen a thread.
json
{ "type": "thread:resolve", "threadId": "thread-uuid", "resolved": true }| Field | Type | Description |
|---|---|---|
threadId | string | Thread to resolve/reopen |
resolved | boolean | true to resolve, false to reopen |
reaction:add
Add an emoji reaction to a comment or thread.
json
{
"type": "reaction:add",
"targetId": "comment-uuid",
"targetType": "comment",
"emoji": "👍"
}| Field | Type | Description |
|---|---|---|
targetId | string | ID of the comment or thread |
targetType | "comment" | "thread" | What to react to |
emoji | string | Emoji character |
reaction:remove
Remove your own reaction.
json
{ "type": "reaction:remove", "reactionId": "reaction-uuid" }notification:read
Mark a single notification as read.
json
{ "type": "notification:read", "notificationId": "notif-uuid" }notification:read-all
Mark all notifications as read.
json
{ "type": "notification:read-all" }click:perform
Send a click indicator visible to other users.
json
{
"type": "click:perform",
"position": { "x": 0.5, "y": 0.3, "pageX": 500, "pageY": 300 }
}typing:start
Send a typing indicator for a thread.
json
{ "type": "typing:start", "threadId": "thread-uuid" }viewport:update
Broadcast your current viewport position.
json
{
"type": "viewport:update",
"scrollX": 0,
"scrollY": 450,
"viewportWidth": 1920,
"viewportHeight": 1080,
"pageWidth": 1920,
"pageHeight": 5000
}| Field | Type | Description |
|---|---|---|
scrollX | number | Horizontal scroll offset |
scrollY | number | Vertical scroll offset |
viewportWidth | number | Viewport width in pixels |
viewportHeight | number | Viewport height in pixels |
pageWidth | number | Total page width in pixels |
pageHeight | number | Total page height in pixels |
selection:update
Broadcast the user's current text selection (or null to clear).
json
{
"type": "selection:update",
"selection": {
"startSelector": "#content > p:nth-child(2)",
"startOffset": 5,
"endSelector": "#content > p:nth-child(2)",
"endOffset": 42
}
}| Field | Type | Description |
|---|---|---|
selection | SelectionRange | null | Current selection, or null to clear |
SelectionRange fields:
| Field | Type | Description |
|---|---|---|
startSelector | string | CSS selector of the start node |
startOffset | number | Character offset within the start node |
endSelector | string | CSS selector of the end node |
endOffset | number | Character offset within the end node |
emoji:drop
Drop an emoji at a position, visible to all users.
json
{
"type": "emoji:drop",
"emoji": "🎉",
"position": { "x": 0.5, "y": 0.3, "pageX": 500, "pageY": 300 }
}draw:stroke
Send a freehand drawing stroke.
json
{
"type": "draw:stroke",
"points": [{ "x": 100, "y": 200 }, { "x": 150, "y": 250 }],
"color": "#ff0000",
"width": 3
}| Field | Type | Description |
|---|---|---|
points | { x: number; y: number }[] | Array of stroke points |
color | string | Stroke color (CSS color string) |
width | number | Stroke width in pixels |
draw:clear
Clear all drawing strokes.
json
{ "type": "draw:clear" }Server → Client Messages
auth:ok
Sent after a successful auth message. Contains the full initial state.
json
{
"type": "auth:ok",
"user": { "id": "env:user-1", "name": "Alice", "color": "#E57373" },
"presence": [],
"threads": [],
"notifications": [],
"reactions": [],
"activityLogs": [],
"users": [],
"config": {
"allowImages": true,
"allowAudio": true,
"allowVideo": true,
"maxFileSizeMb": 10,
"maxAttachmentsPerComment": 5,
"allowReactions": true,
"allowDrawing": true,
"allowMentions": true,
"showCursors": true,
"showPresence": true,
"showTypingIndicators": true
}
}| Field | Type | Description |
|---|---|---|
user | PulseUser | The authenticated user |
presence | PresenceUser[] | Currently online users in the room |
threads | Thread[] | All threads in the room (with comments) |
notifications | Notification[] | Notifications for this user |
reactions | Reaction[] | All reactions in the room |
activityLogs | ActivityLog[] | Recent activity log entries |
users | PulseUser[] | All known users in the room |
config | EnvironmentConfig | Environment feature flags and limits |
auth:error
Sent when authentication fails.
json
{ "type": "auth:error", "message": "Invalid API key" }presence:join
A user joined the room.
json
{
"type": "presence:join",
"user": {
"user": { "id": "env:user-2", "name": "Bob", "color": "#64B5F6" },
"cursor": null,
"status": "online",
"lastSeen": "2026-03-10T14:00:00.000Z",
"deviceType": "desktop"
}
}presence:leave
A user left the room (or timed out).
json
{ "type": "presence:leave", "userId": "env:user-2" }presence:update
A user's presence status changed.
json
{ "type": "presence:update", "userId": "env:user-2", "status": "idle" }cursor:move
A user's cursor moved.
json
{
"type": "cursor:move",
"userId": "env:user-2",
"position": { "x": 0.5, "y": 0.3, "pageX": 500, "pageY": 300 }
}thread:created
A new thread was created.
json
{
"type": "thread:created",
"thread": {
"id": "thread-uuid",
"roomId": "dashboard",
"resolved": false,
"position": { "x": 0.45, "y": 0.72 },
"comments": [{ "id": "comment-uuid", "threadId": "thread-uuid", "userId": "env:user-1", "body": "...", "mentions": [], "attachments": [], "createdAt": "...", "editedAt": null }],
"createdAt": "2026-03-10T14:00:00.000Z",
"updatedAt": "2026-03-10T14:00:00.000Z"
}
}comment:created
A new comment was added to a thread.
json
{
"type": "comment:created",
"threadId": "thread-uuid",
"comment": {
"id": "comment-uuid",
"threadId": "thread-uuid",
"userId": "env:user-2",
"body": "Reply text",
"mentions": [],
"attachments": [],
"createdAt": "2026-03-10T14:05:00.000Z",
"editedAt": null
}
}comment:edited
A comment was edited.
json
{
"type": "comment:edited",
"threadId": "thread-uuid",
"comment": {
"id": "comment-uuid",
"threadId": "thread-uuid",
"userId": "env:user-1",
"body": "Updated text",
"mentions": [],
"attachments": [],
"createdAt": "2026-03-10T14:00:00.000Z",
"editedAt": "2026-03-10T14:10:00.000Z"
}
}comment:deleted
A comment was deleted.
json
{ "type": "comment:deleted", "threadId": "thread-uuid", "commentId": "comment-uuid" }thread:resolved
A thread was resolved or reopened.
json
{ "type": "thread:resolved", "threadId": "thread-uuid", "resolved": true }thread:deleted
A thread was deleted (via REST API).
json
{ "type": "thread:deleted", "threadId": "thread-uuid", "roomId": "dashboard" }reaction:added
A reaction was added.
json
{
"type": "reaction:added",
"reaction": {
"id": "reaction-uuid",
"targetId": "comment-uuid",
"targetType": "comment",
"userId": "env:user-2",
"emoji": "👍",
"createdAt": "2026-03-10T14:20:00.000Z"
}
}reaction:removed
A reaction was removed.
json
{ "type": "reaction:removed", "reactionId": "reaction-uuid", "targetId": "comment-uuid" }notification
A new notification for the current user.
json
{
"type": "notification",
"notification": {
"id": "notif-uuid",
"userId": "env:user-1",
"type": "comment:mention",
"roomId": "dashboard",
"threadId": "thread-uuid",
"commentId": "comment-uuid",
"actorId": "env:user-2",
"read": false,
"createdAt": "2026-03-10T14:05:00.000Z"
}
}click:perform
A user clicked on the page (click indicator).
json
{
"type": "click:perform",
"userId": "env:user-2",
"position": { "x": 0.5, "y": 0.3, "pageX": 500, "pageY": 300 }
}typing:indicator
A user is typing in a thread.
json
{ "type": "typing:indicator", "userId": "env:user-2", "threadId": "thread-uuid" }viewport:update
A user's viewport position changed.
json
{
"type": "viewport:update",
"userId": "env:user-2",
"scrollX": 0,
"scrollY": 450,
"viewportWidth": 1920,
"viewportHeight": 1080,
"pageWidth": 1920,
"pageHeight": 5000
}selection:update
A user's text selection changed.
json
{
"type": "selection:update",
"userId": "env:user-2",
"selection": {
"startSelector": "#content > p:nth-child(2)",
"startOffset": 5,
"endSelector": "#content > p:nth-child(2)",
"endOffset": 42
}
}emoji:drop
A user dropped an emoji.
json
{
"type": "emoji:drop",
"userId": "env:user-2",
"emoji": "🎉",
"position": { "x": 0.5, "y": 0.3, "pageX": 500, "pageY": 300 }
}draw:stroke
A user sent a drawing stroke.
json
{
"type": "draw:stroke",
"userId": "env:user-2",
"points": [{ "x": 100, "y": 200 }, { "x": 150, "y": 250 }],
"color": "#ff0000",
"width": 3
}draw:clear
A user cleared the drawing canvas.
json
{ "type": "draw:clear", "userId": "env:user-2" }activity:logged
A new activity log entry.
json
{
"type": "activity:logged",
"activityLog": {
"id": "log-uuid",
"roomId": "dashboard",
"userId": "env:user-1",
"type": "comment",
"description": "Alice commented on a thread",
"createdAt": "2026-03-10T14:05:00.000Z"
}
}error
A generic error from the server.
json
{ "type": "error", "message": "Something went wrong", "code": "INVALID_THREAD" }| Field | Type | Description |
|---|---|---|
message | string | Human-readable error message |
code | string? | Optional machine-readable error code |
P2P Signaling Messages
These messages are used for WebRTC peer-to-peer connection setup. The server relays them between peers without inspecting the content. See P2P Overview for details.
Client → Server
signal:offer
Send a WebRTC SDP offer to a specific peer.
json
{
"type": "signal:offer",
"targetUserId": "env:user-2",
"sdp": { "type": "offer", "sdp": "v=0\r\n..." }
}signal:answer
Respond to a peer's SDP offer.
json
{
"type": "signal:answer",
"targetUserId": "env:user-1",
"sdp": { "type": "answer", "sdp": "v=0\r\n..." }
}signal:ice
Send an ICE candidate for NAT traversal.
json
{
"type": "signal:ice",
"targetUserId": "env:user-2",
"candidate": { "candidate": "candidate:...", "sdpMid": "0", "sdpMLineIndex": 0 }
}p2p:sync
Broadcast a Yjs CRDT update to all peers via WebSocket (fallback when P2P unavailable).
json
{
"type": "p2p:sync",
"update": "base64-encoded-yjs-update"
}Server → Client
signal:offer / signal:answer / signal:ice
Same as above but with fromUserId instead of targetUserId:
json
{
"type": "signal:offer",
"fromUserId": "env:user-1",
"sdp": { "type": "offer", "sdp": "v=0\r\n..." }
}p2p:sync
Relayed CRDT update from another peer:
json
{
"type": "p2p:sync",
"fromUserId": "env:user-1",
"update": "base64-encoded-yjs-update"
}