Skip to content
BurnerByte

architecture

Real-time & WebSocket

How BurnerByte delivers emails and notifications in real-time.

WebSocket Hubs

BurnerByte uses three separate WebSocket hubs:

Inbox Hub

Endpoint: GET /api/v1/ws/inboxes/{inboxId}?ticket=<ticket>

Streams new emails to a specific inbox in real-time. When the SMTP handler stores a new email, it pushes a notification to the inbox hub, which broadcasts to all connected clients.

Notification Hub

Endpoint: GET /api/v1/ws/notifications?ticket=<ticket>

Streams all notifications for the authenticated user. Powers the notification bell in the top-right corner of the UI.

Admin Stats Hub

Endpoint: GET /api/v1/ws/admin-stats?ticket=<ticket>

Streams real-time platform statistics to system admins. The admin stats worker pushes updated counts (emails, inboxes, domains, users, teams, storage) at regular intervals. Used by the dashboard when auto-refresh is enabled.

Authentication

WebSocket connections do not accept the JWT directly. The client first calls POST /api/v1/ws/ticket with its Bearer token to obtain a single-use ticket (a UUID stored in Redis with a 30-second TTL), then opens the socket with ?ticket=<ticket>. The server validates and consumes the ticket on connect.

Connection Lifecycle

  1. Client calls POST /api/v1/ws/ticket (Bearer auth) and receives a 30-second one-time ticket
  2. Client opens the WebSocket with ?ticket=<ticket>; the server validates and consumes it, then registers the connection
  3. Ping/pong frames maintain the connection (54s ping interval, 60s read deadline)
  4. Write deadline of 10s prevents slow clients from blocking
  5. On disconnect, the connection is unregistered from the hub

Frontend Integration

The useInboxSocket hook manages the WebSocket connection — connects when the inbox detail page mounts, reconnects automatically on disconnect with backoff, calls onNewEmail callback which invalidates TanStack Query cache, and shows a toast notification for each new email.

The NotificationCenter component opens the notification WebSocket and, on each message, raises a toast (plus a browser notification when the tab is hidden) and invalidates its TanStack Query cache. The list itself is server-backed — GET /api/v1/notifications returns the 50 most recent persisted rows — with mark-read, mark-all-read and dismiss endpoints behind it.

Origin Checking

Every socket validates the Origin header against cors.allowed_origins before upgrading. A missing Origin (non-browser clients) is allowed, and * allows everything. The inbox socket matches on hostname; the notification and admin-stats sockets require an exact origin-string match.

The inbox socket applies two further checks before the upgrade: the caller must be the inbox's created_by (403 otherwise), and the inbox must still be active and unexpired (410 otherwise). The admin-stats socket checks is_system_admin and rejects non-admins with 403.

Connection Limits

The inbox hub and the notification hub each cap one user at 5 concurrent connections; a sixth is closed immediately after the upgrade. The admin-stats hub has no per-user cap.

Cross-Process Bridge

cmd/smtpd and cmd/api are separate processes, so the SMTP server cannot write to a socket the API server holds. Redis pub/sub bridges them (internal/realtime/bridge.go):

  • cmd/smtpd publishes an inbox event on bb:inbox after storing an email; the cleanup worker publishes the same envelope for inbox.expired.
  • The API server subscribes at startup and, for each event, broadcasts to the inbox hub, pushes to the owner's notification hub, persists a notifications row, and increments the org/team analytics counters and the daily/hourly rollups. A dropped subscription reconnects after 5 seconds.

With no publisher configured (single-process development), the SMTP handler writes to the in-process hubs directly — no notification row, no analytics increment.