Skip to content

WebSocket Events Reference

The chat-service exposes a Socket.IO namespace at /chat for real-time communication.

Connection

Connecting

typescript
import { io } from 'socket.io-client';

const socket = io('https://api.example.com/chat', {
  auth: {
    token: '<jwt-access-token>',
  },
  transports: ['websocket', 'polling'],
});

Authentication

The gateway validates the JWT at handshake via your IChatUserExtractor. The client must provide a valid Bearer token via auth.token (or an Authorization: Bearer ... header). Connections without a valid token are rejected and disconnected immediately.

userId and tenantId are never read from the handshake — they are extracted from the verified token only. This prevents impersonation: a client cannot join another user's room by passing a fake userId.

On successful auth, the server automatically joins the client to:

  • user:<userId> -- personal room for direct notifications (derived from token)
  • tenant:<tenantId> -- tenant-wide broadcasts (derived from token)

Connection Lifecycle

EventDirectionDescription
connectServerClient successfully connected. Auto-joined to user and tenant rooms.
chat:auth:errorServerEmitted before disconnect when auth fails. Payload: { message: string }.
disconnectServerClient disconnected. Automatically leaves all rooms.

Rooms

The server uses three room types for targeted event delivery:

Room PatternDescriptionJoined via
channel:<channelId>All members currently viewing a channelchat:join:channel event
user:<userId>All socket connections of a single userAutomatic on connect
tenant:<tenantId>All users in a tenantAutomatic on connect

Client to Server Events (4)

These are events the client emits to the server.

chat:typing:start

Notify that the user started typing in a channel.

Payload:

typescript
interface TypingStartPayload {
  channelId: string;
}

Behavior: Broadcasts chat:typing:status:updated to the channel room (excluding the sender) with isTyping: true.


chat:typing:stop

Notify that the user stopped typing.

Payload:

typescript
interface TypingStopPayload {
  channelId: string;
}

Behavior: Broadcasts chat:typing:status:updated to the channel room (excluding the sender) with isTyping: false.


chat:join:channel

Join a channel's WebSocket room to receive real-time updates for that channel.

Payload:

typescript
interface JoinChannelPayload {
  channelId: string;
}

Behavior: Adds the client socket to the channel:<channelId> room. The client will now receive all channel-scoped events.

TIP

Call this when the user opens a channel conversation view. Call chat:leave:channel when they navigate away.


chat:leave:channel

Leave a channel's WebSocket room.

Payload:

typescript
interface LeaveChannelPayload {
  channelId: string;
}

Behavior: Removes the client socket from the channel:<channelId> room.


Server to Client Events (29)

These are events the server emits to connected clients.

Messages (8 events)

chat:message:received

A new message was sent in a channel.

Target room: channel:<channelId>

Payload:

typescript
interface MessageReceivedPayload {
  channelId: string;
  message: {
    id: string;
    channelId: string;
    tenantId: string;
    senderId: string;
    type: 'TEXT' | 'IMAGE' | 'VIDEO' | 'AUDIO' | 'FILE' | 'ADMIN' | 'POLL';
    text: string | null;
    fileUrl: string | null;
    fileName: string | null;
    fileSize: number | null;
    mimeType: string | null;
    thumbnailUrl: string | null;
    parentMessageId: string | null;
    isForwarded: boolean;
    forwardedFromId: string | null;
    isEdited: boolean;
    mentionedUserIds: string[];
    metadata: Record<string, any> | null;
    linkMetadata: Record<string, any> | null;
    pollId: string | null;
    createdAt: string;
    updatedAt: string;
  };
}

chat:message:updated

A message was edited.

Target room: channel:<channelId>

Payload:

typescript
interface MessageUpdatedPayload {
  channelId: string;
  message: {
    id: string;
    text: string;
    isEdited: true;
    updatedAt: string;
  };
}

chat:message:deleted

A message was deleted (soft-delete).

Target room: channel:<channelId>

Payload:

typescript
interface MessageDeletedPayload {
  channelId: string;
  messageId: string;
  deletedAt: string;
}

chat:reaction:updated

A reaction was added or removed on a message.

Target room: channel:<channelId>

Payload:

typescript
interface ReactionUpdatedPayload {
  channelId: string;
  messageId: string;
  reactions: Array<{
    key: string;
    userIds: string[];
    count: number;
  }>;
}

chat:mention:received

The current user was mentioned in a message. Sent to the individual user room.

Target room: user:<userId>

Payload:

typescript
interface MentionReceivedPayload {
  channelId: string;
  message: {
    id: string;
    senderId: string;
    text: string;
    channelId: string;
    createdAt: string;
  };
}

chat:typing:status:updated

A user started or stopped typing in a channel.

Target room: channel:<channelId> (excluding the typing user)

Payload:

typescript
interface TypingStatusPayload {
  channelId: string;
  userId: string;
  isTyping: boolean;
}

TIP

Typing status automatically expires after 5 seconds (CHAT_DEFAULTS.TYPING_TIMEOUT_MS). Clients should stop showing the typing indicator if no isTyping: true is received within that window.


chat:read:receipt:updated

A user marked a channel as read.

Target room: channel:<channelId>

Payload:

typescript
interface ReadReceiptUpdatedPayload {
  channelId: string;
  userId: string;
  lastReadAt: string;
  lastReadMessageId: string | null;
}

chat:pinned:message:updated

A message was pinned or unpinned in a channel.

Target room: channel:<channelId>

Payload:

typescript
interface PinnedMessageUpdatedPayload {
  channelId: string;
  messageId: string;
  action: 'pinned' | 'unpinned';
  pinnedById: string | null;
}

Polls (3 events)

chat:poll:voted

A user voted on a poll.

Target room: channel:<channelId>

Payload:

typescript
interface PollVotedPayload {
  channelId: string;
  pollId: string;
  optionId: string;
  userId: string;
  voterCount: number;
  options: Array<{
    id: string;
    text: string;
    voteCount: number;
  }>;
}

chat:poll:updated

A poll was updated (e.g., closed, new option added).

Target room: channel:<channelId>

Payload:

typescript
interface PollUpdatedPayload {
  channelId: string;
  pollId: string;
  status: 'OPEN' | 'CLOSED';
  voterCount: number;
  options: Array<{
    id: string;
    text: string;
    voteCount: number;
  }>;
  updatedAt: string;
}

chat:poll:deleted

A poll was deleted.

Target room: channel:<channelId>

Payload:

typescript
interface PollDeletedPayload {
  channelId: string;
  pollId: string;
}

Channels (11 events)

chat:channel:changed

A channel's properties were updated (name, cover, customType, etc.).

Target room: channel:<channelId>

Payload:

typescript
interface ChannelChangedPayload {
  channelId: string;
  changes: {
    name?: string;
    coverUrl?: string;
    customType?: string;
    lastMessageAt?: string;
    memberCount?: number;
  };
  updatedAt: string;
}

chat:channel:deleted

A channel was deleted.

Target room: channel:<channelId>

Payload:

typescript
interface ChannelDeletedPayload {
  channelId: string;
  deletedAt: string;
}

chat:user:joined

A user joined or was invited to the channel.

Target room: channel:<channelId>

Payload:

typescript
interface UserJoinedPayload {
  channelId: string;
  userId: string;
  role: 'OPERATOR' | 'MEMBER';
  joinedAt: string;
  memberCount: number;
}

chat:user:left

A user left or was removed from the channel.

Target room: channel:<channelId>

Payload:

typescript
interface UserLeftPayload {
  channelId: string;
  userId: string;
  memberCount: number;
}

chat:unread:count:changed

The user's total unread count changed. Sent to the individual user room.

Target room: user:<userId>

Payload:

typescript
interface UnreadCountChangedPayload {
  totalUnreadCount: number;
}

chat:channel:frozen

A channel was frozen by an operator.

Target room: channel:<channelId>

Payload:

typescript
interface ChannelFrozenPayload {
  channelId: string;
  isFrozen: true;
}

chat:channel:unfrozen

A channel was unfrozen.

Target room: channel:<channelId>

Payload:

typescript
interface ChannelUnfrozenPayload {
  channelId: string;
  isFrozen: false;
}

chat:channel:muted

The current user muted a channel (notification suppression). This is a personal preference — it is emitted only to the acting user's own sockets, never broadcast to other channel members.

Target room: user:<userId> only

Payload:

typescript
interface ChannelMutedPayload {
  channelId: string;
  userId: string;
}

chat:channel:unmuted

The current user unmuted a channel. Like chat:channel:muted, emitted only to the acting user.

Target room: user:<userId> only

Payload:

typescript
interface ChannelUnmutedPayload {
  channelId: string;
  userId: string;
}

chat:metadata:changed

A channel's custom metadata was updated.

Target room: channel:<channelId>

Payload:

typescript
interface MetadataChangedPayload {
  channelId: string;
  metadata: Record<string, string>;
}

chat:channel:hidden

A channel was hidden for the current user. Personal preference — emitted only to the acting user.

Target room: user:<userId> only

Payload:

typescript
interface ChannelHiddenPayload {
  channelId: string;
  userId: string;
}

chat:channel:unhidden

A channel was unhidden (re-shown) for the current user. Personal preference — emitted only to the acting user. Useful for multi-tab sync: if a user unhides a channel in tab A, tab B receives this event to re-display it.

Target room: user:<userId> only

Payload:

typescript
interface ChannelUnhiddenPayload {
  channelId: string;
  userId: string;
}

chat:channel:member:count:changed

The member count of a channel changed (user joined, left, banned, etc.).

Target room: channel:<channelId>

Payload:

typescript
interface MemberCountChangedPayload {
  channelId: string;
  memberCount: number;
}

Moderation (5 events)

chat:user:banned

A user was banned from a channel.

Target room: channel:<channelId> + user:<bannedUserId>

Payload:

typescript
interface UserBannedPayload {
  channelId: string;
  userId: string;
  description: string | null;
  bannedUntil: string | null;
}

chat:user:unbanned

A user was unbanned from a channel.

Target room: channel:<channelId> + user:<unbannedUserId>

Payload:

typescript
interface UserUnbannedPayload {
  channelId: string;
  userId: string;
}

chat:user:muted

A user was muted in a channel (by an operator).

Target room: channel:<channelId> + user:<mutedUserId>

Payload:

typescript
interface UserMutedPayload {
  channelId: string;
  userId: string;
  mutedUntil: string | null;
}

chat:user:unmuted

A user was unmuted in a channel.

Target room: channel:<channelId> + user:<unmutedUserId>

Payload:

typescript
interface UserUnmutedPayload {
  channelId: string;
  userId: string;
}

chat:operator:updated

A user's operator status changed.

Target room: channel:<channelId>

Payload:

typescript
interface OperatorUpdatedPayload {
  channelId: string;
  userId: string;
  role: 'OPERATOR' | 'MEMBER';
}

Event Summary Table

#EventDirectionCategoryTarget Room
1chat:typing:startClient -> ServerMessages-
2chat:typing:stopClient -> ServerMessages-
3chat:join:channelClient -> ServerConnection-
4chat:leave:channelClient -> ServerConnection-
5chat:message:receivedServer -> ClientMessageschannel:<id>
6chat:message:updatedServer -> ClientMessageschannel:<id>
7chat:message:deletedServer -> ClientMessageschannel:<id>
8chat:reaction:updatedServer -> ClientMessageschannel:<id>
9chat:mention:receivedServer -> ClientMessagesuser:<id>
10chat:typing:status:updatedServer -> ClientMessageschannel:<id>
11chat:read:receipt:updatedServer -> ClientMessageschannel:<id>
12chat:pinned:message:updatedServer -> ClientMessageschannel:<id>
13chat:poll:votedServer -> ClientPollschannel:<id>
14chat:poll:updatedServer -> ClientPollschannel:<id>
15chat:poll:deletedServer -> ClientPollschannel:<id>
16chat:channel:changedServer -> ClientChannelschannel:<id>
17chat:channel:deletedServer -> ClientChannelschannel:<id>
18chat:user:joinedServer -> ClientChannelschannel:<id>
19chat:user:leftServer -> ClientChannelschannel:<id>
20chat:unread:count:changedServer -> ClientChannelsuser:<id>
21chat:channel:frozenServer -> ClientChannelschannel:<id>
22chat:channel:unfrozenServer -> ClientChannelschannel:<id>
23chat:channel:mutedServer -> ClientChannelsuser:<id>
24chat:channel:unmutedServer -> ClientChannelsuser:<id>
25chat:metadata:changedServer -> ClientChannelschannel:<id>
26chat:channel:hiddenServer -> ClientChannelsuser:<id>
27chat:channel:unhiddenServer -> ClientChannelsuser:<id>
28chat:channel:member:count:changedServer -> ClientChannelschannel:<id>
29chat:user:bannedServer -> ClientModerationchannel:<id> + user:<id>
30chat:user:unbannedServer -> ClientModerationchannel:<id> + user:<id>
31chat:user:mutedServer -> ClientModerationchannel:<id> + user:<id>
32chat:user:unmutedServer -> ClientModerationchannel:<id> + user:<id>
33chat:operator:updatedServer -> ClientModerationchannel:<id>

Total: 33 events — 4 client→server + 29 server→client.

MIT License