WebSockets
FreeSDN exposes a persistent WebSocket connection at /api/v1/ws that delivers every platform event in real time: device state changes, alert firings, scan progress, agent heartbeats, camera detections, VoIP call events, automation completions, and more. The same transport carries commands to connected agents. This page covers the two WS endpoints, the authentication handshake, the message protocol, subscription filtering, and the security model the server enforces on each connection.
Endpoints
Section titled “Endpoints”| Method | Path | Purpose | Auth |
|---|---|---|---|
WS | /api/v1/ws | Real-time event stream and agent-command channel | JWT (see below) |
GET | /api/v1/ws/stats | Returns the count of currently active connections | authenticated HTTP |
The stats endpoint is a plain HTTP GET - use it for dashboards or health checks when you want to know how many clients are connected without opening a socket yourself.
Opening a connection
Section titled “Opening a connection”Use any standards-compliant WebSocket client. The URL scheme is wss:// in production and ws:// in development:
wss://your-domain/api/v1/wsThe server performs two checks during connection setup:
- Origin validation - for browser connections the
Originheader must matchCORS_ORIGINSor be same-origin. Connections that fail this check are closed immediately with code 1008 beforeaccept()is called. Agent connections (non-browser) are not subject to the Origin check. - Connection caps - the server enforces
MAX_WS_PER_USER=25concurrent connections per user account andMAX_WS_GLOBAL=5000across all tenants. Exceeding either cap closes the new connection with code 1013 (Try Again Later). When the connection authenticates via cookie or query-string token the cap is enforced beforeaccept(). When the connection authenticates via the auth-message frame,accept()must be called first (so the server can receive the frame); if the cap is exceeded after the auth message arrives the already-upgraded socket is closed with 1013.
Authentication
Section titled “Authentication”You must authenticate within 10 seconds of the connection being accepted. Three mechanisms are supported, in order of preference:
1. Auth-message frame (recommended)
Section titled “1. Auth-message frame (recommended)”Send a JSON frame immediately after the socket opens:
{"type": "auth", "token": "<JWT access token>"}The server validates the token with the same verify_token logic used by the REST API. On success it sends a connected frame listing all available event types. On failure it closes the connection.
2. Session cookie
Section titled “2. Session cookie”If the browser already has the freesdn_access httpOnly cookie set (from a prior /api/v1/auth/login call), the cookie is read during the upgrade handshake automatically. No extra frame is needed.
3. Query-string token (deprecated)
Section titled “3. Query-string token (deprecated)”wss://your-domain/api/v1/ws?token=<JWT>What the principal carries
Section titled “What the principal carries”The authenticated principal attached to the connection holds the user’s user_id, org_id, role, permission list, and token_version. The token_version is checked at connection time and re-validated every five minutes (see Session revalidation below).
API keys
Section titled “API keys”Message protocol
Section titled “Message protocol”All messages are JSON objects with a type field that identifies the message kind.
Server → client messages
Section titled “Server → client messages”type | When sent | Key fields |
|---|---|---|
connected | Immediately after successful auth | available_events (array of all EventType values) |
event | When a subscribed event fires | event object (see Event shape) |
subscribed | After a successful subscribe request | subscriptions (confirmed patterns) |
unsubscribed | After an unsubscribe request | subscriptions (removed patterns) |
subscription_denied | On a subscribe request for a pattern the caller lacks permission for | patterns (denied list) |
filters_set | After a successful set_filters request | site_ids |
pong | In response to a ping | timestamp (ISO 8601 server time) |
session_revoked | When the server detects the session is no longer valid | - |
error | Protocol or validation error | message |
Event shape
Section titled “Event shape”{ "type": "event", "event": { "event_type": "device.state_changed", "category": "devices", "priority": "normal", "organization_id": "<uuid>", "payload": { "site_id": "<uuid or null>", "..." } }}Payloads are sanitized before delivery: the server strips any field whose name appears in the _SENSITIVE_PAYLOAD_KEYS set - password, hashed_password, secret, api_key, api_secret, client_secret, token, access_token, refresh_token, private_key, ssh_key, mfa_secret, mfa_backup_codes, credentials, cookie, session_token, and encryption_key - from every outgoing payload, regardless of nesting depth.
Client → server messages
Section titled “Client → server messages”Send these frames after authentication. Frames arrive at a rate limiter capped at five messages per second; exceeding that closes the connection with code 1008.
subscribe
Section titled “subscribe”{ "type": "subscribe", "subscriptions": [ "device.*", "alert.fired", "discovery.*" ]}Patterns may use wildcards. Each pattern must be 200 characters or fewer. The server checks every requested pattern against the permission map (see Subscription permissions) and responds with a subscribed frame for allowed patterns and a subscription_denied frame for any that are denied. You can hold up to MAX_SUBSCRIPTIONS_PER_CONN=200 active subscriptions per connection.
unsubscribe
Section titled “unsubscribe”{"type": "unsubscribe", "subscriptions": ["alert.fired"]}set_filters
Section titled “set_filters”Narrow the event stream to one or more sites. The server validates each site_id against your organisation membership and your UserSiteAccess grants - any site you cannot read is rejected silently.
{"type": "set_filters", "site_ids": ["<uuid>", "<uuid>"]}{"type": "ping"}The server responds with {"type": "pong", "timestamp": "<ISO 8601 datetime>"}. Use this to keep the connection alive through idle-timeout proxies and to measure round-trip latency.
Subscription permissions
Section titled “Subscription permissions”Not every role can subscribe to every event stream. The server maps subscription patterns to permission requirements in app/core/ws_rbac.py. The general principle mirrors the REST permission model:
| Event prefix | Required permission | Notes |
|---|---|---|
device.* | device:read | Device state changes, adoption events |
alert.* | alert:read | Alert fired/resolved |
sla.* | analytics:read | SLA status updates |
controller.* | controller:read | Controller sync events |
discovery.* | device:read | Scan progress and discovered-device events |
vpn.* | vpn:read | VPN tunnel state |
pbx.* | device:read | PBX sync progress and call-state events |
camera.* | device:read | Camera health and detection events |
nvr.* | device:read | NVR-level events (reboot, recording status) |
audit.* | audit:read | Audit log entries |
security.* | audit:read | Security event stream |
settings.* | settings:read | Platform settings changes |
user.* | user:read | User account events |
admin.* | super_admin only | Administrative events |
system.* | super_admin only | Internal system events |
The viewer role carries device:read, alert:read, controller:read, vpn:read, analytics:read, and audit:read by default, giving read-only users a broad real-time view that includes device state, alerts, controller sync events, VPN tunnel state, SLA updates, audit log entries, and security events. Only user.* (requires user:read), settings.* (requires settings:read), admin.*, and system.* streams are out of reach for viewers. Unknown prefixes (for example network.*, cameras.*, voip.*, firewall.*, agent.*, automation.*) are denied by default - the server sends a subscription_denied frame and the subscription is silently rejected.
The FreeSDN React frontend uses the same WebSocket endpoint. It reads the freesdn_access httpOnly cookie automatically and does not require an auth-message frame because the cookie is sent with the upgrade request.
Next steps
Section titled “Next steps”- Authentication - obtain a JWT access token to use in the auth-message frame
- Authentication - obtain a short-lived JWT access token via
POST /api/v1/auth/loginfor use in non-interactive WS clients (API key WS support is not yet available) - Using the API - CSRF, rate limits, and error shapes that also apply to WebSocket clients
- Agents - agent registration, task dispatch, and the REST endpoints that complement the WS channel