Skip to content

WebSocket Protocol

This page catalogs Brokkr’s three WebSocket surfaces and the wire messages they carry. For the design rationale, lane prioritization, and operating guidance, see Internal Broker↔Agent WS Channel.

The wire types are defined in the brokkr-wire crate. They are not part of the OpenAPI surface and are not generated into the SDKs; external integrators use the REST API. WIRE_VERSION equals the crate version, which is released in lockstep with the broker and SDKs.

Endpoint Direction Auth Purpose
GET /internal/ws/agent bidirectional Agent PAK only (admin and generator PAKs are rejected) Internal broker↔agent channel: control-plane pushes down, telemetry up
GET /api/v1/stacks/{id}/live server → client Admin PAK or generator PAK owning the stack (agent PAKs are not accepted in v1) Read-only live tail of a stack’s telemetry frames
GET /api/v1/fleet/live server → client Admin PAK only (same gate as GET /fleet) Read-only consumer-facing live stream of per-agent fleet records

These endpoints are standard HTTP→WebSocket upgrades served by the broker.

Non-browser clients send the PAK as a normal header on the upgrade request:

Authorization: Bearer <PAK>

Browsers cannot set headers on new WebSocket(). For /api/v1/stacks/{id}/live and /api/v1/fleet/live, a browser client instead offers the PAK as a subprotocol:

Sec-WebSocket-Protocol: brokkr.pak.<PAK>, brokkr.v1

The broker extracts the PAK from the brokkr.pak. subprotocol (consulted only when no Authorization header is present) and echoes back only the non-secret brokkr.v1 marker.

Every frame is a JSON-encoded WsMessage with an external tag:

{ "type": "<variant>", "body": { ... } }

Variant names are snake_case.

type Body Meaning
work_order WorkOrder (same shape as REST) A work order this agent may claim was created
target_changed AgentTarget Targets changed — re-poll. Sent when a stack target is added for this agent, and also as a synthetic nudge when the agent is deregistered from a generator
stack_changed Stack A targeted stack’s metadata or deployment objects changed

Control pushes are hints: they are fire-and-forget, sent after the database commit, and the agent’s REST polling remains the source of truth (see ADR-0008).

target_changed should be read as a signal to re-poll, not as a description of what changed. Two cases carry a body that does not describe a real added target:

  • Deregistration nudge. When an agent is deregistered from a generator, the broker sends target_changed with a synthetic AgentTarget whose id and stack_id are the nil UUID. The agent inspects only the variant; a consumer that parses the body as an added target is misled.
  • Removals are not pushed. Deleting an individual target sends no frame in v1. The removal surfaces on the agent’s next REST poll.
type Body Meaning
heartbeat { agent_id, sent_at } plus optional k8s_reachable and k8s_api_latency_ms (the source of the fleet record’s k8s signals) Liveness signal sent on the agent’s poll tick while the connection is up
agent_event AgentEvent (same shape as REST) Deployment SUCCESS/FAILURE event
agent_health DeploymentHealth (same shape as REST) Health status for one deployment object
type Body Meaning
k8s_event K8sEvent A Kubernetes Event for a stack-managed object
pod_log_line PodLogLine One container log line from an opted-in pod
log_gap LogGap Marker that lines were dropped, so consumers render a visible gap

Body shapes for the telemetry-only types:

// k8s_event
{
"agent_id": "<uuid>", "stack_id": "<uuid>",
"observed_at": "<ISO-8601>",
"reason": "FailedScheduling", "message": "...",
"event_type": "Warning", "source": "scheduler",
"involved_object": {
"api_version": "v1", "kind": "Pod",
"namespace": "default", "name": "myapp-abc12", "uid": "<uid>"
}
}
// pod_log_line
{
"agent_id": "<uuid>", "stack_id": "<uuid>",
"namespace": "default", "pod": "myapp-abc12", "container": "myapp",
"ts": "<ISO-8601>", "line": "..."
}
// log_gap
{
"agent_id": "<uuid>", "stack_id": "<uuid>",
"since_ts": "<ISO-8601>", "dropped_count": 42,
"reason": "rate_limit"
}

log_gap.reason is one of rate_limit, buffer_full, disconnected.

Carried only on GET /api/v1/fleet/live. The broker pushes one frame whenever it observes a discrete fleet event for an agent — a broker↔agent WebSocket connect/disconnect, or a heartbeat receipt. It also pushes a frame from a periodic ~20-second sweep when an agent’s computed signals (deployment-object backpressure counts or deployment-health counts) have changed since the last tick. See Fleet Reference for the complete trigger model. The consumer pulls GET /fleet once for the baseline, then replaces a row in place keyed by agent_id.

type Body Meaning
fleet_update FleetAgentRecord One agent’s full fleet record (measured values only — same shape as a GET /fleet entry); replace by agent_id

The fleet_update body is the wire twin of the REST FleetAgentRecord, field-for-field identical to the GET /fleet element — measured signals only, no health verdicts:

// fleet_update
{
"agent_id": "<uuid>",
"name": "demo-agent",
"cluster_name": "demo-cluster",
"status": "ACTIVE",
"ws_connected": true,
"connected_since": "<ISO-8601 | null>",
"last_heartbeat": "<ISO-8601 | null>",
"heartbeat_age_seconds": 0,
"pending_object_count": 0,
"pending_work_orders": 0,
"claimed_work_orders": 0,
"last_event_at": "<ISO-8601 | null>",
"seconds_since_last_event": 0,
"health_failing": 0,
"health_degraded": 0,
"k8s_reachable": true,
"k8s_api_latency_ms": 12
}

Unlike the per-stack live tail, the fleet stream has no gap marker: a slow subscriber that lags simply drops the missed frames and continues, because the next fleet_update for that agent_id supersedes any it missed (the consumer holds the latest record per agent).

Property Value Enforced by
Per-connection control lane capacity 64 messages, drained before telemetry Broker
Per-connection telemetry lane capacity 1024 messages Broker
Live-tail broadcast capacity (per stack) 1024 frames; lagged subscribers receive a synthetic log_gap Broker
Fleet live-push broadcast capacity (fleet-wide) 1024 frames; lagged subscribers drop and continue (no gap marker — replace-by-agent_id) Broker
Agent outbound/inbound queues 256 messages each; a full outbound lane falls back to REST Agent
Agent reconnect backoff Exponential, 1s initial, 60s max Agent
Auth-rejection limit 5 consecutive 401/403s, then the agent stops dialing until restart Agent
Telemetry retention 6-hour hard ceiling, evicted by server-side created_at Broker

WebSocket activity is exposed via the brokkr_ws_* Prometheus metrics (see Monitoring) and the per-connection snapshot at GET /api/v1/admin/ws/connections. Consumer-facing fleet live-push subscribers are counted by the brokkr_fleet_live_subscribers gauge.