WECHESS

FRONTEND GUIDE FOR AI CODING AGENTS - PART 10 - LobbyChat Service Realtime Hubs

This document is a part of a REST API guide for the wechess project. It is designed for AI agents that will generate frontend code to consume the project’s backend.

This document provides the Realtime Hub integration guide for the LobbyChat service. Realtime Hubs use Socket.IO for bidirectional communication between clients and the server, enabling features like chat rooms, live collaboration, game lobbies, and dashboards.

Connection Setup

Service URLs

The Socket.IO server runs on the same host as the REST API for the lobbyChat service:

Authentication

Every Socket.IO connection must include the user’s access token and the correct path for the reverse proxy:

import { io } from "socket.io-client";

const socket = io("{baseUrl}/hub/{hubName}", {
  path: "/lobbychat-api/socket.io/",  // HTTP transport path (reverse proxy)
  auth: { token: accessToken },           // Bearer token from login
  transports: ["websocket", "polling"],    // prefer websocket
});

Where {baseUrl} is the service base URL (e.g., https://wechess.mindbricks.co/lobbychat-api).

How Socket.IO works behind a reverse proxy:

The server validates the token and resolves the user session before allowing any interaction. If the token is invalid or missing, the connection is rejected with an "Authentication required" or "Invalid token" error.

Connection Lifecycle

connect  →  authenticate  →  join rooms  →  send/receive  →  leave rooms  →  disconnect

Listen for connection events:

socket.on("connect", () => {
  console.log("Connected to hub", socket.id);
});

socket.on("connect_error", (err) => {
  console.error("Connection failed:", err.message);
  // Handle re-auth if token expired
});

socket.on("disconnect", (reason) => {
  console.log("Disconnected:", reason);
});

Protocol Reference

All hub events use the hub: prefix. Below is the complete protocol that applies to every hub in this service.

Room Management

Event Direction Payload Description
hub:join Client → Server { roomId, meta? } Join a room. meta is optional user metadata broadcast to others.
hub:joined Server → Client { roomId, hubRole, userInfo } Confirmation that the client successfully joined. userInfo contains { fullname, avatar }.
hub:leave Client → Server { roomId } Leave a room.
hub:error Server → Client { roomId?, error } Error response for any failed operation.
hub:presence Server → Room { event, roomId, user } Broadcast when a user joins or leaves. event is "joined" or "left". user includes { id, fullname, avatar, hubRole }.

Join a room:

socket.emit("hub:join", { roomId: "room-abc-123" });

socket.on("hub:joined", ({ roomId, hubRole, userInfo }) => {
  console.log("Joined room", roomId, "as", hubRole);
  // userInfo = { fullname: "John Doe", avatar: "https://..." }
});

socket.on("hub:presence", ({ event, roomId, user }) => {
  // user = { id, fullname, avatar, hubRole }
  if (event === "joined") showUserJoined(roomId, user);
  if (event === "left") showUserLeft(roomId, user);
});

socket.on("hub:error", ({ error }) => {
  console.error("Error:", error);
});

Sending Messages

Event Direction Payload
hub:send Client → Server { roomId, messageType, content, replyTo?, forwarded? }
hub:messageArrived Server → Room { roomId, sender, message }sender includes { id, fullname, avatar }

Send a message:

socket.emit("hub:send", {
  roomId: "room-abc-123",
  messageType: "text",
  content: { body: "Hello everyone!" }
});

Receive messages:

socket.on("hub:messageArrived", ({ roomId, sender, message }) => {
  // sender = { id, fullname, avatar }
  // message = { id, messageType, content, timestamp, senderName, senderAvatar, ... }
  addMessageToUI(roomId, sender, message);
});

History

When joining a room (if history is enabled), the server automatically sends the most recent messages right after the hub:joined event. Each message in the history array includes senderName and senderAvatar fields, so the frontend can render user display names and avatars without additional lookups.

Event Direction Payload
hub:history Server → Client { roomId, messages[] }

Automatic history on join:

socket.on("hub:history", ({ roomId, messages }) => {
  // messages are ordered newest-first, each has: id, roomId, senderId,
  // senderName, senderAvatar, messageType, content, timestamp, status
  renderMessageHistory(roomId, messages);
});

Paginated history via REST (for “load more” / scroll-up):

For older messages beyond the initial batch, use the REST endpoint:

GET /{hubKebabName}/{roomId}/messages?limit=50&offset=50
Authorization: Bearer {accessToken}

Response: { data: Message[], pagination: { total: number } }

Each message in the REST response also contains senderName and senderAvatar.

Custom Events

Event Direction Payload
hub:event Client → Server { roomId, event, data }
hub:{eventName} Server → Room { roomId, userId, ...data }
// Emit a custom event
socket.emit("hub:event", {
  roomId: "room-abc-123",
  event: "customAction",
  data: { key: "value" }
});

// Listen for custom events
socket.on("hub:customAction", ({ roomId, userId, ...data }) => {
  handleCustomEvent(roomId, userId, data);
});

Hub Definitions

Hub: lobbyChatHub

Namespace: /lobbychat-api/hub/lobbyChatHub Description: Public lobby chat hub. A single shared room where all authenticated users (guests and registered) can chat in real-time. Messages have 24-hour retention. Supports moderation: report, mute, and admin removal.

Connection

const lobbyChatHubSocket = io("{baseUrl}/lobbychat-api/hub/lobbyChatHub", {
  path: "/lobbychat-api/socket.io/",
  auth: { token: accessToken },
  transports: ["websocket", "polling"]
});

Room Settings

Setting Value
Room DataObject lobbyRoom
Absolute Roles (bypass auth) administrator

Authorization Sources (checked in order, first match wins):

# Name Source Object User Field Room Field Hub Role Condition
1 allowAllAuthenticated lobbyRoom id id lobbyUser

Hub Roles

Role Read Send Allowed Types Moderated Moderate Delete Any Manage Room
lobbyUser Yes Yes text No No No No
moderator Yes Yes all No Yes Yes No

Users with absoluteRoles get a built-in system role with all permissions.

Message Types

Messages are stored in the lobbyChatHubMessage DataObject with the following structure:

Field Type Description
id ID Primary key
roomId ID Reference to the room
senderId ID Reference to the sending user
senderName String Display name of the sender (denormalized at send time)
senderAvatar String Avatar URL of the sender (denormalized at send time)
messageType Enum One of: text, system
content JSON Type-specific payload (see below)
timestamp DateTime Message creation time
reaction JSON Emoji reactions [{ emoji, userId, timestamp }]

Built-in Message Types

Each message type requires specific fields in the content object:

text

Field Type Required
body Text Yes
socket.emit("hub:send", {
  roomId,
  messageType: "text",
  content: { body: "..." }
});

system

Field Type Required
systemAction String Yes
systemData JSON No
socket.emit("hub:send", {
  roomId,
  messageType: "system",
  content: { systemAction: "..." }
});

Cross-cutting Features

Reactions: Reactions are stored on the message object. Use the message DataObject’s update API to add/remove reactions.

Standard Events

Event Client Emits Server Broadcasts
Typing hub:event with event: "typing" hub:typing { roomId, userId }
Stop Typing hub:event with event: "stopTyping" hub:stopTyping { roomId, userId }
Presence Online (automatic on connect) hub:online { roomId, userId }
Presence Offline (automatic on disconnect) hub:offline { roomId, userId }

Example — Typing indicator:

// Start typing
socket.emit("hub:event", { roomId, event: "typing" });

// Stop typing (call after a debounce timeout)
socket.emit("hub:event", { roomId, event: "stopTyping" });

// Listen for others typing
socket.on("hub:typing", ({ roomId, userId }) => {
  showTypingIndicator(roomId, userId);
});
socket.on("hub:stopTyping", ({ roomId, userId }) => {
  hideTypingIndicator(roomId, userId);
});

Custom Events

userMuted — Broadcast when a user is muted in the lobby by an admin. Direction: serverToRoom

// Emit
socket.emit("hub:event", {
  roomId,
  event: "userMuted",
  data: { /* your payload */ }
});

// Listen
socket.on("hub:userMuted", ({ roomId, userId, ...data }) => {
  // handle event
});

messageModerated — Broadcast when a message is removed by admin moderation, so all users can hide it. Direction: serverToRoom

// Emit
socket.emit("hub:event", {
  roomId,
  event: "messageModerated",
  data: { /* your payload */ }
});

// Listen
socket.on("hub:messageModerated", ({ roomId, userId, ...data }) => {
  // handle event
});

Auto-Bridged Server Events

These events are automatically emitted to rooms when DataObject changes occur on the backend (via Kafka). The frontend only needs to listen:

Event Trigger Payload
hub:messageEdited lobbyChatHubMessage updated DataObject record fields
hub:messageDeleted lobbyChatHubMessage deleted DataObject record fields
hub:roomUpdated lobbyRoom updated DataObject record fields
hub:roomClosed lobbyRoom deleted DataObject record fields
socket.on("hub:messageEdited", ({ roomId, ...data }) => {
  handleMessageEdited(roomId, data);
});
socket.on("hub:messageDeleted", ({ roomId, ...data }) => {
  handleMessageDeleted(roomId, data);
});
socket.on("hub:roomUpdated", ({ roomId, ...data }) => {
  handleRoomUpdated(roomId, data);
});
socket.on("hub:roomClosed", ({ roomId, ...data }) => {
  handleRoomClosed(roomId, data);
});

Moderation Commands

Users with canModerate permission can block, silence, and manage messages:

Block/Unblock a user:

socket.emit("hub:block", { roomId, userId: targetUserId, reason: "Spam", duration: 3600 });
socket.emit("hub:unblock", { roomId, userId: targetUserId });

Silence/Unsilence a user:

socket.emit("hub:silence", { roomId, userId: targetUserId, reason: "Off-topic", duration: 600 });
socket.emit("hub:unsilence", { roomId, userId: targetUserId });

Duration is in seconds. 0 or omitted = permanent.

Listen for moderation actions on your user:

socket.on("hub:blocked", ({ roomId, reason }) => {
  // You have been blocked — leave UI, show message
});
socket.on("hub:unblocked", ({ roomId }) => {
  // Block lifted — you may rejoin the room
});
socket.on("hub:silenced", ({ roomId, reason }) => {
  // You have been silenced — disable send button
});
socket.on("hub:unsilenced", ({ roomId }) => {
  // Silence lifted — re-enable send button
});

REST API Endpoints

In addition to Socket.IO, the hub exposes REST endpoints for message history and management:

Get message history:

GET /lobby-chat-hub/{roomId}/messages?limit=50&offset=0

Response:

{
  "success": true,
  "data": [ /* message objects */ ],
  "pagination": { "limit": 50, "offset": 0, "total": 120 }
}

Send a message via REST:

POST /lobby-chat-hub/{roomId}/messages
Content-Type: application/json
Authorization: Bearer {accessToken}

{
  "data": { "body": "Hello from REST" },
  "replyTo": null
}

Messages sent via REST are also broadcast to all connected Socket.IO clients in the room.

Delete a message:

DELETE /lobby-chat-hub/{roomId}/messages/{messageId}
Authorization: Bearer {accessToken}

Guardrails

Limit Value
Max users per room 1000
Max rooms per user 3
Message rate limit 20 msg/min
Max message size 4096 bytes
Connection timeout 300000 ms
History on join Last 50 messages

Frontend Integration Checklist

  1. Install socket.io-client: npm install socket.io-client
  2. Create a connection manager that handles connect/disconnect/reconnect with token refresh.
  3. Join rooms after connection. Listen for hub:joined before sending messages. The hub:joined event includes the user’s hubRole and userInfo (fullname, avatar).
  4. Render chat history from the hub:history event that arrives automatically after joining. Each message includes senderName and senderAvatar for display.
  5. Handle hub:error globally for all error responses.
  6. Use sender info from hub:messageArrived events — the sender object includes { id, fullname, avatar }. For history messages, use the stored senderName and senderAvatar fields.
  7. Parse messageType to render different message bubbles (text, image, video, etc.).
  8. Use REST endpoints for paginated history when scrolling up in a conversation (GET /{hubKebabName}/{roomId}/messages?limit=50&offset=50).
  9. Debounce typing indicators — emit typing on keypress, stopTyping after 2–3 seconds of inactivity.
  10. Track read receipts per room to show unread counts and read status.
  11. Handle presence to show online/offline status. The hub:presence event includes user.fullname and user.avatar for display.
  12. Reconnect gracefully — re-join rooms and fetch missed messages via REST on reconnect.

Example: Full Chat Integration

import { io } from "socket.io-client";

class ChatHub {
  constructor(baseUrl, token) {
    this.socket = io(`${baseUrl}/lobbychat-api/hub/lobbyChatHub`, {
      path: "/lobbychat-api/socket.io/",
      auth: { token },
      transports: ["websocket", "polling"]
    });

    this.rooms = new Map();
    this._setupListeners();
  }

  _setupListeners() {
    this.socket.on("hub:joined", ({ roomId, hubRole, userInfo }) => {
      this.rooms.set(roomId, { joined: true, hubRole, userInfo, messages: [], members: new Map() });
    });

    this.socket.on("hub:history", ({ roomId, messages }) => {
      const room = this.rooms.get(roomId);
      if (room) room.messages = messages;
      // Each message has senderName and senderAvatar for display
    });

    this.socket.on("hub:presence", ({ event, roomId, user }) => {
      const room = this.rooms.get(roomId);
      if (!room) return;
      if (event === "joined") {
        room.members.set(user.id, { fullname: user.fullname, avatar: user.avatar, hubRole: user.hubRole });
      } else if (event === "left") {
        room.members.delete(user.id);
      }
    });

    this.socket.on("hub:messageArrived", ({ roomId, sender, message }) => {
      // sender = { id, fullname, avatar }
      const room = this.rooms.get(roomId);
      if (room) room.messages.push(message);
      this.onNewMessage?.(roomId, sender, message);
    });

    this.socket.on("hub:error", ({ error }) => {
      console.error("[ChatHub]", error);
    });

    this.socket.on("hub:typing", ({ roomId, userId }) => {
      this.onTyping?.(roomId, userId, true);
    });
    this.socket.on("hub:stopTyping", ({ roomId, userId }) => {
      this.onTyping?.(roomId, userId, false);
    });

    this.socket.on("hub:online", ({ roomId, userId }) => {
      this.onPresence?.(userId, "online");
    });
    this.socket.on("hub:offline", ({ roomId, userId }) => {
      this.onPresence?.(userId, "offline");
    });
  }

  joinRoom(roomId) {
    this.socket.emit("hub:join", { roomId });
  }

  leaveRoom(roomId) {
    this.socket.emit("hub:leave", { roomId });
    this.rooms.delete(roomId);
  }

  sendMessage(roomId, messageType, content, options = {}) {
    this.socket.emit("hub:send", {
      roomId,
      messageType,
      content,
      ...options
    });
  }

  sendTyping(roomId) {
    this.socket.emit("hub:event", { roomId, event: "typing" });
  }

  sendStopTyping(roomId) {
    this.socket.emit("hub:event", { roomId, event: "stopTyping" });
  }

  disconnect() {
    this.socket.disconnect();
  }
}

After this prompt, the user may give you new instructions to update the output of this prompt or provide subsequent prompts about the project.