Skip to content

WebSocket protocol

The native WebSocket endpoint streams live query results: register a query once and receive a fresh snapshot whenever a mutation changes its result. This page is the frame-level contract. For a working client walkthrough, see Use the native HTTP and WebSocket API.

PropertyValue
EndpointGET /ws (HTTP upgrade)
Subprotocolnimbus.v2, offered via Sec-WebSocket-Protocol
TenantX-Tenant-Id header or tenant_id query parameter
FramesJSON text frames only

The upgrade is rejected before any WebSocket traffic when:

ConditionResult
Sec-WebSocket-Protocol missing or without nimbus.v2HTTP 400, code protocol.no_overlap
Tenant missing from header and queryHTTP 400, code op.invalid_input
Tenant does not existHTTP 404, code session.tenant_not_found
Missing or invalid credential (server with local security)HTTP 401, code auth.unauthorized

The protocol.no_overlap error’s detail lists serverSupports (["nimbus.v2"]) and clientOffered.

After the upgrade the server immediately sends a hello frame:

{
"type": "hello",
"protocol": "nimbus.v2",
"server": { "version": "<server version>", "build": "<build id>" },
"features": ["queries.v1", "subscriptions.v1"],
"session": { "id": "<session id>", "serverNow": 1765386000000 }
}

serverNow is the server clock in epoch milliseconds. The client must reply with a client_hello text frame within 10 seconds:

{ "type": "client_hello", "protocol": "nimbus.v2" }

Handshake violations end the connection with a fatal_error frame followed by a close frame with code 1008 (policy violation); the close reason is the error code:

ViolationError code
No client_hello within 10 secondsprotocol.hello_timeout (detail.timeoutMs)
Frame is not valid JSONprotocol.invalid_json
First frame’s type is not client_helloprotocol.unsupported_message_type (detail.receivedType, detail.expectedType)
protocol is not nimbus.v2protocol.unsupported_version (detail.receivedProtocol)
Binary frame during handshakeprotocol.unsupported_binary

Ping frames are answered with pongs during the handshake. After the handshake, binary frames are ignored.

All client frames are JSON objects tagged by type.

{
"type": "subscribe",
"request_id": "messages-1",
"query": { "table": "messages", "filters": [] }
}
  • request_id — caller-chosen string echoed in the first result and in registration errors.
  • query — the same query object as POST /query in the HTTP API reference: table and filters required, order and limit optional.

If registration fails, the server sends an op.error frame carrying the request_id and code op.failed.

{ "type": "unsubscribe", "subscription_id": 7 }

subscription_id is the numeric id from subscription_result frames. If teardown fails the server sends an error frame with code session.unsubscribe_failed.

{ "type": "clear_auth" }

The server replies with {"type": "authenticated", "is_authenticated": false}.

Not supported on the native route. The server replies with an error frame whose error code is auth.unauthorized and message authentication is not supported on the generic websocket route. The connection stays open.

typeFieldsMeaning
hellosee handshakeSent once after upgrade
subscription_resultsubscription_id, data, request_id (first result only)Full current result set for the subscribed query
authenticatedis_authenticatedReply to clear_auth
errorerrorSession-level error; connection stays open
op.errorid, errorError tied to a specific request_id
fatal_errorerrorTerminal error; followed by close code 1008
{
"type": "subscription_result",
"subscription_id": 7,
"request_id": "messages-1",
"data": [
{ "_id": "...", "_creationTime": 1765386000000, "_updateTime": 1765386000000, "text": "hello" }
]
}

data is always the complete current result set, not a delta. The first result for a subscription includes the originating request_id; later updates carry only subscription_id. Each connection has a single ordered writer: frames arrive in the order the server emits them.

CodeFrameTrigger
protocol.invalid_jsonerrorText frame that is not a valid client message
op.failedop.errorSubscription registration or evaluation failed for a known request_id
session.subscription_errorerrorSubscription stream error not tied to a request_id
session.unsubscribe_failederrorunsubscribe teardown failed
auth.unauthorizederrorauthenticate sent on the native route

The error object inside every frame uses the envelope fields documented in the error reference.

Subscriptions are connection-scoped. After a disconnect, reconnect, complete the handshake, and re-send subscribe for each query. The first subscription_result after resubscribing is a full snapshot, so no client-side replay is needed.