The WebSocket API provides real-time communication for chat messages, reactions, presence tracking, and typing indicators.
Connection
Endpoint: GET /api/ws
Authentication uses the session cookie set at login — no token in the URL or headers is needed. The server validates the session cookie automatically on upgrade.
// Cookie is sent automatically by the browser
const ws = new WebSocket("wss://your-meepa-instance.example.com/api/ws");
Origin validation is enforced for browser clients. Non-browser clients (mobile apps, desktop, bots) that omit the Origin header are allowed through.
Connection Parameters
| Parameter | Value |
|---|
| Max message size | 4096 bytes |
| Ping interval | Server sends WebSocket ping frames every 30 seconds |
| Pong timeout | Client must respond within 60 seconds or the connection is closed |
| Write deadline | 10 seconds |
Client-to-Server Events
All client messages must be JSON with this structure:
{
"type": "event_type",
"data": { ... }
}
ping
Client heartbeat. The server responds with a pong event.
subscribe
Subscribe to one or more channels to receive their messages and events.
{
"type": "subscribe",
"data": {
"channelIds": ["550e8400-e29b-41d4-a716-446655440000"]
}
}
unsubscribe
Unsubscribe from channels to stop receiving their events.
{
"type": "unsubscribe",
"data": {
"channelIds": ["550e8400-e29b-41d4-a716-446655440000"]
}
}
typing
Broadcast a typing indicator to other users in a channel. The server excludes the sender from the broadcast.
{
"type": "typing",
"data": {
"channelId": "550e8400-e29b-41d4-a716-446655440000"
}
}
Server-to-Client Events
All server messages follow the same JSON structure:
{
"type": "event_type",
"data": { ... }
}
pong
Response to a client ping event.
presence.initial
Sent immediately after connection. Contains all currently online user IDs.
{
"type": "presence.initial",
"data": {
"userIds": [
"550e8400-e29b-41d4-a716-446655440000",
"6ba7b810-9dad-11d1-80b4-00c04fd430c8"
]
}
}
presence.update
Broadcast when a user goes online or offline. Sent to all connected clients.
{
"type": "presence.update",
"data": {
"userId": "550e8400-e29b-41d4-a716-446655440000",
"status": "online"
}
}
The status field is either "online" or "offline".
typing
Broadcast when a user is typing in a channel. Only sent to other users subscribed to that channel (the sender is excluded).
{
"type": "typing",
"data": {
"channelId": "550e8400-e29b-41d4-a716-446655440000",
"userId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
}
}
message.created
Broadcast when a new message is posted. Only sent to users subscribed to that channel.
{
"type": "message.created",
"data": {
"id": "msg-id",
"channelId": "channel-id",
"userId": "user-id",
"content": "Hello, world!",
"createdAt": "2026-02-14T12:34:56Z",
"updatedAt": "2026-02-14T12:34:56Z",
"threadId": null,
"attachments": []
}
}
message.updated
Broadcast when a message is edited. Only sent to users subscribed to that channel.
{
"type": "message.updated",
"data": {
"id": "msg-id",
"channelId": "channel-id",
"userId": "user-id",
"content": "Hello, world! (edited)",
"createdAt": "2026-02-14T12:34:56Z",
"updatedAt": "2026-02-14T12:35:30Z",
"threadId": null,
"attachments": []
}
}
message.deleted
Broadcast when a message is deleted. Only sent to users subscribed to that channel.
{
"type": "message.deleted",
"data": {
"messageId": "550e8400-e29b-41d4-a716-446655440000",
"channelId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
}
}
reaction.sync
Broadcast when reactions are added or removed from a message. Contains the full reaction state for that message — not a diff.
{
"type": "reaction.sync",
"data": {
"messageId": "550e8400-e29b-41d4-a716-446655440000",
"reactions": {
"\ud83d\udc4d": ["6ba7b810-9dad-11d1-80b4-00c04fd430c8"],
"\u2764\ufe0f": [
"7c9e6679-7425-40de-944b-e07fc1f90ae7",
"550e8400-e29b-41d4-a716-446655440000"
]
}
}
}
The reaction.sync event always contains the complete reaction state for the message. Replace your local reaction state entirely rather than trying to merge it.
channel.created
Broadcast when a new channel is created. Sent to all connected clients.
{
"type": "channel.created",
"data": {
"id": "channel-id",
"serverId": "server-id",
"name": "general",
"type": "text",
"groupId": "group-id",
"position": 0,
"createdAt": "2026-02-14T12:34:56Z"
}
}
channel.updated
Broadcast when a channel is renamed, moved to a different group, or reordered. Sent to all connected clients.
{
"type": "channel.updated",
"data": {
"id": "channel-id",
"serverId": "server-id",
"name": "announcements",
"type": "text",
"groupId": "group-id",
"position": 1,
"createdAt": "2026-02-14T12:34:56Z"
}
}
channel.deleted
Broadcast when a channel is deleted. Sent to all connected clients.
{
"type": "channel.deleted",
"data": {
"channelId": "550e8400-e29b-41d4-a716-446655440000",
"serverId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
}
}
channel.member_added
Broadcast to a channel when members are added. Sent to all subscribers of that channel (including the newly added members).
{
"type": "channel.member_added",
"data": {
"channelId": "550e8400-e29b-41d4-a716-446655440000",
"channel": { "id": "...", "name": "...", "serverId": "...", "type": "text" },
"userIds": ["6ba7b810-9dad-11d1-80b4-00c04fd430c8"]
}
}
dm.opened
Sent directly to a user when someone opens a new DM conversation with them. Not broadcast to all clients — only the recipient receives it.
{
"type": "dm.opened",
"data": {
"channel": { "id": "...", "type": "dm" },
"user": { "id": "...", "username": "alice" }
}
}
Reconnection Strategy
Clients should implement automatic reconnection with exponential backoff:
- On disconnect, wait 1 second before the first retry
- Double the wait time on each failed attempt (max 30 seconds)
- Reset the wait time after a successful connection
- Re-subscribe to all channels after reconnecting
- Fetch missed messages via the REST API based on the last received message timestamp
let reconnectDelay = 1000;
const maxDelay = 30000;
function connect() {
// Session cookie is sent automatically
const ws = new WebSocket("wss://your-meepa-instance.example.com/api/ws");
ws.onopen = () => {
console.log('Connected');
reconnectDelay = 1000; // reset
// Re-subscribe to channels
ws.send(JSON.stringify({
type: 'subscribe',
data: { channelIds: myChannels }
}));
};
ws.onclose = () => {
console.log(`Disconnected, reconnecting in ${reconnectDelay}ms`);
setTimeout(connect, reconnectDelay);
reconnectDelay = Math.min(reconnectDelay * 2, maxDelay);
};
ws.onerror = (err) => {
console.error('WebSocket error:', err);
};
ws.onmessage = (msg) => {
const event = JSON.parse(msg.data);
handleEvent(event);
};
}
Error Handling
The WebSocket connection will close with the following codes:
| Code | Name | Description |
|---|
| 1000 | Normal Closure | Clean disconnect |
| 1006 | Abnormal Closure | Connection lost (network issue, server restart) |
| 1008 | Policy Violation | Authentication failed or origin mismatch |
If the connection closes abnormally (code 1006 or 1008), clients should attempt to reconnect using the strategy described above. For code 1008, verify that your token is still valid before reconnecting.