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
- Client calls
POST /api/v1/ws/ticket(Bearer auth) and receives a 30-second one-time ticket - Client opens the WebSocket with
?ticket=<ticket>; the server validates and consumes it, then registers the connection - Ping/pong frames maintain the connection (54s ping interval, 60s read deadline)
- Write deadline of 10s prevents slow clients from blocking
- 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/smtpdpublishes an inbox event onbb:inboxafter storing an email; the cleanup worker publishes the same envelope forinbox.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
notificationsrow, 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.