WebSocket Protocol Reference
Endpoint: GET /ws (upgrade to WebSocket)
Transport: JSON messages over WebSocket
Source: packages/server/src/ws/handler.ts, packages/server/src/ws/events.ts
Auth Flow
- Client connects to
/ws
- Client sends
{ type: 'auth', token: '<jwt>' } within 10 seconds
- Server validates token (rejects deleted users, tokens issued before
passwordChangedAt)
- Server responds with
ready event containing full client state
- Server updates user status to
online, broadcasts presence_update to all user's spaces
- Heartbeat: server pings every 30s (RFC 6455 ping frames), dead connections detected after ~65s
Client → Server
Messages
| type |
fields |
notes |
message_create |
channelId, content, replyToId? |
SEND_MESSAGES perm |
message_edit |
messageId, content |
author only |
message_delete |
messageId |
author or MANAGE_MESSAGES |
typing_start |
channelId |
5s auto-expire |
DM Messages
| type |
fields |
notes |
dm_message_create |
dmChannelId, content?, attachments?, replyToId? |
member |
dm_message_edit |
messageId, content |
author only |
dm_message_delete |
messageId |
author only |
dm_typing_start |
dmChannelId |
5s auto-expire |
Reactions (space + DM, auto-detected)
| type |
fields |
notes |
reaction_add |
messageId, emoji |
ADD_REACTIONS perm (space) |
reaction_remove |
messageId, emoji |
own reactions only |
Read State
| type |
fields |
notes |
channel_ack |
channelId, messageId |
mark read up to message |
mark_unread |
channelId, messageId |
'0' to clear all |
Presence & Activity
| type |
fields |
notes |
presence_update |
status: online/idle/dnd |
persisted to DB |
activity_update |
activities: Activity[] |
rate-limited 3s, respects showActivity |
Voice (Space Channels)
| type |
fields |
notes |
voice_join |
channelId |
one room per user enforced |
voice_leave |
— |
|
voice_status |
isMuted, isDeafened, isCameraOn, isScreenSharing |
server enforces space/permission mute |
Voice Moderation
| type |
fields |
permission |
voice_space_mute |
userId, muted |
MUTE_MEMBERS |
voice_space_deafen |
userId, deafened |
DEAFEN_MEMBERS |
voice_move |
userId, targetChannelId |
MOVE_MEMBERS |
voice_disconnect |
userId |
DISCONNECT_MEMBERS |
DM Calls
| type |
fields |
notes |
dm_call_start |
dmChannelId |
60s auto-timeout if not accepted |
dm_call_accept |
dmChannelId |
ringing→active |
dm_call_reject |
dmChannelId |
|
dm_call_end |
dmChannelId |
|
System
| type |
fields |
auth |
token |
ping |
— (gets pong) |
Server → Client
System
| type |
fields |
scope |
ready |
(see Ready Payload below) |
user |
pong |
— |
user |
error |
message |
user |
Messages
| type |
fields |
scope |
message_created |
message: MessageWithUser |
channel (VIEW_CHANNEL) |
message_updated |
message: MessageWithUser |
channel |
message_deleted |
messageId, channelId |
channel |
typing |
channelId, userId, username |
channel (excludes sender) |
reaction_added |
messageId, reaction (includes user) |
channel |
reaction_removed |
messageId, userId, emoji |
channel |
embeds_resolved |
messageId, channelId, embeds[] |
channel |
DM Messages
| type |
fields |
scope |
dm_message_created |
message: DmMessageWithUser |
DM members |
dm_message_updated |
message: DmMessageWithUser |
DM members |
dm_message_deleted |
messageId, dmChannelId |
DM members |
dm_typing |
dmChannelId, userId, username |
DM members (excludes sender) |
dm_embeds_resolved |
messageId, dmChannelId, embeds[] |
DM members |
Read State
| type |
fields |
scope |
channel_ack |
channelId, messageId |
user (multi-tab sync) |
mark_unread |
channelId, messageId |
user (multi-tab sync) |
Presence & Activity
| type |
fields |
scope |
presence_update |
userId, status, activities? |
space (all members) |
user_updated |
user |
user |
Space / Channel Management
| type |
fields |
scope |
space_updated |
space |
space |
member_joined |
spaceId, member: MemberWithUser |
space |
member_left |
spaceId, userId |
space |
member_banned |
spaceId, reason |
user (banned) |
channel_created |
channel, spaceId |
space |
channel_updated |
channel, spaceId |
space |
channel_deleted |
channelId, spaceId |
space |
category_created |
category, spaceId |
space |
category_updated |
category, spaceId |
space |
category_deleted |
categoryId, spaceId |
space |
channel_layout_updated |
spaceId, channels[], categories[] |
space |
space_layout_updated |
layout[], folders[], updatedAt? |
user |
DM Channel Management
| type |
fields |
scope |
dm_channel_created |
dmChannel |
user |
dm_channel_closed |
dmChannelId |
user |
dm_member_added |
dmChannelId, user |
DM members |
dm_member_removed |
dmChannelId, userId |
DM members |
dm_owner_updated |
dmChannelId, newOwnerId |
DM members |
Voice
| type |
fields |
scope |
voice_state_update |
channelId, userId, action: join/leave |
space |
voice_status_update |
userId, channelId, isMuted, isDeafened, isCameraOn, isScreenSharing |
room |
voice_space_muted |
userId, channelId, spaceId, muted |
space |
voice_space_deafened |
userId, channelId, spaceId, deafened |
space |
voice_permission_muted |
userId, spaceId, muted |
space |
voice_moved |
userId, oldChannelId, newChannelId |
user (target) |
voice_disconnected |
userId, channelId, reason? |
user (target) |
reason: 'displaced' (new tab) |
'session_closed' |
|
DM Calls
| type |
fields |
scope |
dm_call_incoming |
dmChannelId, callerId, callerName |
DM members (excludes caller) |
dm_call_accepted |
dmChannelId |
DM members |
dm_call_rejected |
dmChannelId |
DM members |
dm_call_ended |
dmChannelId |
DM members |
Social
| type |
fields |
scope |
friend_request_received |
request |
user (target) |
friend_request_accepted |
friend, requestId |
user (requester) |
friend_request_declined |
requestId, userId |
user (requester) |
friend_request_cancelled |
requestId, userId |
user (target) |
friend_removed |
userId |
user |
Discovery
| type |
fields |
scope |
join_request_received |
request |
space (managers) |
join_request_accepted |
request, space |
user (requester) |
join_request_declined |
request |
user (requester) |
Federation
| type |
fields |
scope |
federation_file_rejected |
messageId, dmChannelId, attachmentId, affectedUsers[] |
DM members |
Ready Payload