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 asAuthorization: Bearerfor 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
integrationIdand 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, andproject.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.
-
Same origin as REST
The browser widget and dashboard build URLs like
wss://<API_HOST>/api/v1/chat/.... EnsureAPI_PUBLIC_URL/ public DNS point at the same host running this process (or a stable load balancer in front). -
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.
-
Proxy: Upgrade support
For
/api/v1/chat/wsand/api/v1/chat/visitor-ws(and for the API in general if WS shares the samelocation) you need:
-
HTTP/1.1 to the upstream;
-
UpgradeandConnectionheaders (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.
-
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.
-
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.
-
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, sessionschat-widget-nextjs-example{.interpreted-text role="doc"} --- Next.js BFF and WebSocket origin env- OpenAPI:
https://api.mailoo.app/docs/v1