DocsCollaboration

Chat

Channels, DMs, voice, reactions, and read receipts, over a shared Socket.IO gateway.

Channel types#

ChatChannel.type is one of ORGANIZATION (persistent, organized into categories, e.g. #general), REPOSITORY (exactly one per repository, created lazily on first access), DIRECT, or GROUP. Direct channels are deduplicated by a canonical dmKey (the two participants' user IDs, sorted and joined) so re-opening a DM with the same person always resolves to the same channel rather than creating a duplicate.

Transport#

Real-time delivery runs over a Socket.IO gateway at /api/socket.io, authenticated during the WebSocket handshake using the same khub_access cookie (or a bearer token for non-browser clients) as REST requests — there's no separate chat-specific credential. Clients join per-resource rooms:

RoomJoined for
channel:<channelId>Receiving new messages, edits, deletes, reactions, and read-receipt updates for one conversation.
repo:<repositoryId>Live "this repository was updated" notifications (e.g. to refresh a file browser after a push), independent of that repo's chat channel.
voice:<channelId>Presence and WebRTC signaling relay for a voice channel or an ad-hoc audio/video call — the server only relays signaling payloads between participants' socket IDs; media itself is peer-to-peer.

Messages#

Beyond plain text (Markdown-free by design, to keep chat fast and low-ceremony), a message can carry:

  • A reply reference (replyToId) — rendered as a quoted snippet of the original linking back to it; replying to a message from another channel is rejected server-side.
  • Reactions — a (messageId, userId, emoji) row per reaction, grouped by emoji for display with a count and whether the current viewer has reacted, so toggling a reaction is idempotent from the client's perspective (react/unreact, not add/remove-by-index).
  • @mentions (@username) and inline org/project/repo references, extracted server-side by regex at send time to drive notification fan-out.

Read receipts#

There is no per-message read-receipt table. Instead, each ChatChannelMember carries a single lastReadAt timestamp, updated the moment a member opens the channel. A message is considered "read" by a given member if that member's lastReadAt is at or after the message's createdAt — which is what powers the single-check/double-check indicator on a sent message without needing to fan out a write per recipient per message.

Notification integration#

Posting a message notifies every other channel member who wasn't already notified via an @mention (mentions and general channel-activity notifications are deduplicated, not stacked). Opening a channel doesn't just advance lastReadAt — it also marks any notification pointing at that channel's link as read in the same request, which is what makes the chat and notification-bell unread counts converge the instant you actually look at the conversation rather than requiring two separate "mark read" actions. See Notifications.