Skip to main content
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

ParameterValue
Max message size4096 bytes
Ping intervalServer sends WebSocket ping frames every 30 seconds
Pong timeoutClient must respond within 60 seconds or the connection is closed
Write deadline10 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.
{
  "type": "ping"
}

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.
{
  "type": "pong"
}

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:
  1. On disconnect, wait 1 second before the first retry
  2. Double the wait time on each failed attempt (max 30 seconds)
  3. Reset the wait time after a successful connection
  4. Re-subscribe to all channels after reconnecting
  5. 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:
CodeNameDescription
1000Normal ClosureClean disconnect
1006Abnormal ClosureConnection lost (network issue, server restart)
1008Policy ViolationAuthentication 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.