Chat WebSocket --- operation and production API

Last updated: Sep 21, 2026Section: Integrations

This section describes how chat (JSBOX) WebSocket channels work in the Mailoo API, which events they carry, and what you must configure on the public API host in production (proxy, TLS, scaling).

Chat integration overview (REST, keys, BFF): chat-widget{.interpreted-text role="doc"} and chat-widget-nextjs-example{.interpreted-text role="doc"}.

::: {.contents local=""} :::

Where the WebSocket lives

The WebSocket does not listen on a separate port. The same Node.js process that serves HTTP (Hono) creates an http.Server and, on upgrade, attaches clients to ws (WebSocketServer with noServer: true). Paths are fixed in server code:

  • Owner (dashboard) --- /api/v1/chat/ws
  • Visitor (widget) --- /api/v1/chat/visitor-ws

In production, clients connect to the same host and scheme as the REST API, for example wss://api.example.com/api/v1/chat/ws?.... You do not need a separate "WebSocket port" --- HTTPS/WSS on the published API service (usually 443 behind a load balancer) is enough.

Two channels and authentication

Owner (dashboard): /api/v1/chat/ws

Query parameters: integrationId, token.

  • token --- Keycloak access JWT (same as Authorization: Bearer for REST). In development, a configured bypass may accept a dev token (as in the app).
  • After upgrade, the API verifies the user and that the integration exists, type is JSBOX, and it belongs to that user.
  • On success the client registers in the in-memory hub by integrationId and receives {"type":"connected","integrationId":"..."}.

Visitor: /api/v1/chat/visitor-ws

Query parameters: projectUid, integrationId, sessionPublicId.

  • No API key: trust comes from an existing chat session in the DB (created by the first webhook message) with matching publicId, integrationId, and project.uid.
  • On success --- hub registration for integrationId + sessionPublicId, response {"type":"connected","sessionPublicId":"..."}.

Hub and events

Implementation is an in-memory module (connection maps per integration and per session). When a new chat message appears (inbound from visitor or outbound owner reply), the server broadcasts JSON such as:

{
  "type": "chat.message",
  "payload": {
    "messageId": "...",
    "integrationId": "...",
    "sessionPublicId": "...",
    "messageType": "INBOUND|OUTBOUND",
    "content": "...",
    "senderEmail": "...",
    "senderName": null,
    "createdAt": "..."
  }
}

Clients use this to refresh the UI without full REST polling. If WebSocket is unavailable, the app may fall back to polling (as in the chat console or widget).

Production checklist (public Mailoo API)

Checklist for teams exposing the Mailoo API on the internet. A separate "WebSocket port" is not published: you need a correct reverse proxy, TLS, and a clear scaling model.

  1. Same origin as REST

    The browser widget and dashboard build URLs like wss://<API_HOST>/api/v1/chat/.... Ensure API_PUBLIC_URL / public DNS point at the same host running this process (or a stable load balancer in front).

  2. TLS and ``wss:``

    On an HTTPS site the widget must use WSS. The proxy needs valid certificates; the path to the API matches ordinary HTTPS.

  3. Proxy: Upgrade support

    For /api/v1/chat/ws and /api/v1/chat/visitor-ws (and for the API in general if WS shares the same location) you need:

  • HTTP/1.1 to the upstream;

  • Upgrade and Connection headers (typical WebSocket pattern);

  • increased read timeouts for long-lived connections (tens of minutes / hours), or the proxy will drop quiet sockets.

    Example nginx fragment (same upstream as REST):

    location / {
      proxy_pass http://api_backend;
      proxy_http_version 1.1;
      proxy_set_header Upgrade $http_upgrade;
      proxy_set_header Connection "upgrade";
      proxy_set_header Host $host;
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header X-Forwarded-Proto $scheme;
      proxy_read_timeout 3600s;
      proxy_send_timeout 3600s;
    }
    

    Ensure your reverse proxy (or cloud load balancer) forwards WebSocket Upgrade/Connection headers and uses long read/send timeouts on the API location.

  1. Firewall / security groups

    Same rules as a normal HTTPS API (for example inbound 443 to the load balancer). Outbound to DB, Keycloak, S3 --- per your architecture; WebSocket does not change that.

  2. Multiple API replicas (important)

    The current chat hub lives only in process memory. An owner connection on replica A will not see a broadcast performed on replica B if another pod handled the message.

    Options:

  • temporarily run a single API replica for reliable real-time;

  • or enable sticky sessions (client pinned to a pod) by cookie/IP on the load balancer --- this only helps if message-create webhooks and both WebSockets land on the same instance (unreliable with multiple workers);

  • true horizontal real-time scaling needs a shared broker (Redis Pub/Sub, etc.) --- not implemented in the current codebase.

    Document the chosen strategy in your production runbook.

  1. Site side (not the API)

    The Next.js BFF and env such as MAILOO_CHAT_WS_ORIGIN / API base must point at the public API host that serves WSS. That is configured on the site app, not as a separate WebSocket publish step.

Short summary


Question Answer


Separate WebSocket port? No --- same service and path as REST (via 443 and the proxy).

What must the proxy support? Upgrade/WebSocket, long timeouts, TLS for wss.

Anything to expose besides HTTP(S)? No, if the API is already published over HTTPS.

Multiple API pods? In-memory hub: single replica, sticky sessions, or a future event bus.

See also

  • chat-widget{.interpreted-text role="doc"} --- REST, keys, CORS, sessions
  • chat-widget-nextjs-example{.interpreted-text role="doc"} --- Next.js BFF and WebSocket origin env
  • OpenAPI: https://api.mailoo.app/docs/v1