Streams
Streams are the subscribe/publish half of the WebSocket
protocol. You subscribe to a named stream with a filter; the server
sends event_* messages whenever new data matches.
Snapshots are deduped and retained — when you subscribe, the server sends you the latest known snapshot for each matching resource immediately, then continues with deltas as they arrive.
Stream names
The StreamName enum lists what's available:
| Stream | Carries | Source |
|---|---|---|
device_snapshot |
Per-device live state (position, temperature, exposure state, …) | SkyNode for each connected device |
telescope_snapshot |
Per-telescope aggregate state | SkyNode |
instrument_snapshot |
Per-instrument state — combinations of devices that together carry out an observation type | Computed from device snapshots |
telescope_log |
Telescope log lines — significant events, errors, operator notes | SkyNode |
constraint_snapshot |
Operational constraint definitions and current thresholds | SkyNode |
constraint_status_snapshot |
Constraint pass/fail status with latest evaluation | SkyNode |
manual_control_lease |
Manual control lease lifecycle events | Hub |
shutdown_interlock_snapshot |
Active shutdown interlocks | SkyNode |
Subscribing
Send a subscribe_stream message identifying the stream and filter:
{
"kind": "subscribe_stream",
"subscription_id": "<client-chosen id>",
"stream": "device_snapshot",
"filter": {
"telescope_uid": "00000000-0000-0000-0000-000000000000"
},
"options": {
"include_existing": true
}
}
The subscription_id is a client-chosen string used to correlate
deliveries with the subscription. The server includes it on every
event_* message delivered for that subscription.
The filter shape is per-stream. Common filter keys:
telescope_uid— restrict to one telescope.observatory_uid— restrict to one observatory.device_type— restrict to a device kind.device_uid— restrict to a specific device.
When options.include_existing is true (default), the server
immediately replays the current retained state for each matching
resource before streaming new deliveries. Set false if you only
want fresh data.
To stop receiving:
{
"kind": "unsubscribe_stream",
"subscription_id": "<the same id>"
}
Receiving
Deliveries arrive as event_<stream> messages:
{
"kind": "event_device_snapshot",
"subscription_id": "<your-subscription-id>",
"snapshot": { /* DeviceSnapshot record */ }
}
The shape of snapshot matches the corresponding schema:
event_device_snapshot→DeviceSnapshot(with polymorphicdeviceTypediscriminator)event_telescope_snapshot→TelescopeSnapshotevent_instrument_snapshot→InstrumentSnapshotevent_constraint_snapshot→OperationalConstraintevent_constraint_status_snapshot→ConstraintStatusSnapshotevent_manual_control_lease→ManualControlLeaseevent_telescope_log→TelescopeLogEntry
Publishing (SkyNode only)
SkyNode installations publish snapshots back to the hub through the same WebSocket. The publish messages have shape:
{
"kind": "publish_device_snapshot",
"snapshot": { /* DeviceSnapshot */ }
}
These are not useful for external integrations — only SkyNode installations are authorized to publish. They're listed here for completeness so the protocol is fully described.
Patterns
Per-telescope dashboard
Subscribe to all of device_snapshot,
constraint_status_snapshot, telescope_log, and
manual_control_lease filtered to your telescope's UID. That gives
you everything a dashboard needs: live device state, constraint
go/no-go, log entries, and operator takeover events.
Multi-telescope monitoring
For network-wide monitoring (fleet dashboards), subscribe to
telescope_snapshot with no filter — you'll receive an initial
snapshot per telescope and updates as they change.
Backfill from a known cursor
The protocol doesn't currently expose a cursor — event_* messages
are deltas from the moment of subscription forward (plus the
initial retained state if include_existing is true). To backfill
historical state, use the REST API's snapshot endpoints, then
subscribe for new data.
Lifecycle
- Keep-alive — exchange
ping/pongto keep the connection warm. Inactive connections may be closed by intermediaries. - Reconnect — on disconnect, reconnect with the same access token and re-subscribe. The server doesn't persist subscriptions across reconnects.
- Error — when an
errormessage arrives, log thecodeandmessage. Aforbiddenerror means your token doesn't have permission for the requested stream/filter — get a token with appropriate scope rather than retrying.
Reference
The full message envelope and per-stream shapes are at
packages/py/skynet-sdk/skynet_sdk/schemas/ws_protocol.py.
The router is at
apps/public-api/public_api/routers/ws.py
and
apps/public-api/public_api/routers/ws/.