Realtime Layer
Meww.me uses two WebSocket-based systems for real-time communication between the bot and the dashboard frontend.
Architecture
Frontend (React) Fastify (port 2555) Bot Core
┌──────────────┐ ┌───────────────────┐
│ │ WS/ │ Socket.IO │
│ useSocket() │ � - �────── │ SocketBridge │
│ usePlayer() │ ──────► │ │
│ │ │ ┌──────────┐ │
└──────────────┘ │ │ BotWs │ │
│ │ Service │ │
│ └────┬─────┘ │
└─────────┼─────────┘
│ internal WS
┌─────────▼─────────┐
│ websocket.ts │
│ (Fastify WS) │
│ /v1/ws/:guildId │
└───────────────────┘There are two distinct WebSocket layers:
- Internal WebSocket - A Fastify WebSocket route (
websocket.ts) that connects the bot core to theBotWsService. This is an internal channel. - Socket.IO - A Socket.IO server attached to the Fastify HTTP server. The
SocketBridgeconnects Socket.IO clients (the frontend) to the internal WebSocket.
Socket.IO (Frontend ↔ Dashboard)
Connection
The frontend connects to the Socket.IO server at the VITE_BACKEND_URL with the following configuration:
Transport: polling → websocket (upgrade)
Ping interval: 10,000ms
Ping timeout: 8,000ms
Credentials: true (sends cookies for session auth)CORS
The Socket.IO server applies the same CORS policy as the Fastify REST API. Allowed origins are determined by FRONTEND_URL in config. In development, localhost:3000 and localhost:3001 are automatically allowed.
Event Flow
Subscribe to a Guild
Client → Server: subscribe(guildId)
Server → Client: subscribed({ guildId })
Server → Client: playerCreate(playerState) // initial state, if player existsThe client joins a Socket.IO room named guild:{guildId}. The SocketBridge also subscribes to the internal WebSocket for that guild and forwards all events.
Request State Refresh
Client → Server: requestState()
Server → Client: playerState({ ...state, _guild: guildId })Used when the frontend needs to reconcile its local state with the bot's actual state.
Unsubscribe
Client → Server: unsubscribe()
// or:
Client disconnects → automatic cleanupThe SocketBridge cleans up the internal WebSocket subscription and removes the client from the Socket.IO room.
Events Emitted by the Server
| Event | Payload | Description |
|---|---|---|
subscribed | { guildId } | Confirmation that the client joined the guild room |
playerCreate | Player state object | Initial state when subscribing (if a player exists) |
playerState | Player state object | Full state snapshot (response to requestState) |
trackStart | Track + player data | A new track has started playing |
trackEnd | Track data | The current track has ended |
queueUpdate | Queue data | The queue has been modified |
playerUpdate | Player state diff | Player properties changed (volume, loop, pause, etc.) |
playerDestroy | { guildId } | The player has been destroyed |
All server-emitted events include a _guild field with the guild ID for routing.
Frontend Integration
The useSocket hook manages the Socket.IO connection:
// Simplified usage in usePlayer.ts
const socket = useSocket();
useEffect(() => {
if (!socket || !guildId) return;
socket.emit("subscribe", guildId);
socket.on("playerState", (data) => {
// Update local player state
});
socket.on("trackStart", (data) => {
// Update current track
});
return () => {
socket.emit("unsubscribe");
socket.off("playerState");
socket.off("trackStart");
};
}, [socket, guildId]);Internal WebSocket (websocket.ts)
The internal WebSocket route is available at /v1/ws/:guildId on the Fastify server. It requires the same Authorization header as the REST API.
This WebSocket is used exclusively by BotWsService to receive real-time events from the bot core. The bot emits events using manager.dashboardEmit(guildId, event, data), which internally emits to the Socket.IO room for that guild.
SocketBridge (SocketBridge.ts)
The SocketBridge class connects the two WebSocket layers:
- When a Socket.IO client subscribes to a guild, the bridge subscribes the internal
BotWsServiceto that guild's events. - Internal events are forwarded to the Socket.IO room.
- On disconnect, the bridge cleans up both the Socket.IO room membership and the internal subscription.
class SocketBridge {
private subscriptions = new Map<string, { guildId: string; handler: Function }>();
constructor(
private io: SocketIOServer,
private botWs: BotWsService,
private botApi: AxiosInstance
) {
this.setup();
}
// subscribe: join room, subscribe to internal WS, fetch initial state
// unsubscribe: leave room, unsubscribe from internal WS
// disconnect: automatic cleanup
}Each Socket.IO socket can only be subscribed to one guild at a time. Subscribing to a new guild automatically unsubscribes from the previous one.
Dashboard Emit
The bot core can push events to dashboard clients at any time via:
manager.dashboardEmit(guildId, eventName, data);This function is set by the DashboardPlugin during the onReady hook:
client.dashboardEmit = (guildId, event, data) =>
io.to('guild:' + guildId).emit(event, data);Events are emitted from various locations:
- Player events (
trackStart.ts,trackEnd.ts) - push track changes - Queue modifications - push queue updates
- Settings changes - push configuration updates
- Player state changes - push pause, volume, loop changes
Player State Object
The player state object emitted in real-time events contains:
{
"guildId": "123456789012345678",
"current": {
"title": "Song Title",
"uri": "https://...",
"length": 240000,
"thumbnail": "https://...",
"author": "Artist Name"
},
"queue": [
{ "title": "...", "uri": "...", "length": 0, "thumbnail": "...", "author": "..." }
],
"pause": false,
"position": 45000,
"volume": 80,
"loop": "none",
"autoplay": false,
"updatedAt": 1714000000000
}The updatedAt timestamp is used by the frontend for state reconciliation when events arrive out of order.