Skip to content

Websocket & Events – Application Guide

This chapter explains how frontend applications and external services use the Websocket API of the coldwave backend to receive live updates and events.

It focuses on:

  • How to open a websocket connection (ticket-based and direct)
  • How to send healthcheck pings and handle reconnects
  • How events from different modules (Service, Streams, Meta, Alarms, Notes, IAM) are delivered and how to route them in your app
  • How IAM permissions and device/service scoping apply to events

The low-level event schemas remain documented in the individual module pages (Service, Streams, Meta, Alarms, Notes, IAM, …). This guide explains how to put everything together in an application.

TIP

This guide describes the second iteration of the websocket module. Refer to the backend configuration if you need to run v1 and v2 in parallel.


1. When and why to use websockets

The REST API gives you the current state (services, properties, meta, …).
The websocket gives you changes over time, for example:

  • A property value changes (OBJECT_UPDATED from the Service module)
  • A livestream emits new samples (BIN_STREAM_UPDATE / STRING_STREAM_UPDATE)
  • A device goes online/offline/idle (META_STATUS_CHANGE)
  • An alarm becomes active or inactive (ALARM_STATUS_ACTIVE / ALARM_STATUS_INACTIVE)
  • A note is added to a device (NOTE_MESSAGE_ADD)
  • An IAM object changes (API keys, users, roles, …)

Typical uses in a UI:

  • Realtime device dashboards
  • Live log/trace views
  • “Online/Offline” badges
  • Alarm center, notification counters
  • Collaborative tools (notes, admin consoles)

The recommended pattern is:

  1. Load initial state via REST (e.g. /api/v1/devices, /api/v1/meta, /api/v1/alarms/status)
  2. Open a websocket
  3. Apply incoming events to your local state store

2. Connection options

The websocket module exposes three endpoints:

  • GET /api/v1/events/ticket – create a short-lived ticket for browsers
  • GET /api/v1/events/:ticket – open a websocket using a ticket
  • GET /api/v1/events – open a websocket with an Authorization header

All endpoints authenticate the connection using the same IAM token that is used for REST calls. The IAM permissions and device/service scoping fully apply to which events you will see.

2.1 Browser / SPA: ticket-based connection

Most browser WebSocket implementations do not allow setting arbitrary HTTP headers (like Authorization) during the upgrade request. For that scenario, use the ticket flow:

  1. Your SPA calls GET /api/v1/events/ticket with the regular Bearer token:

    http
    GET /api/v1/events/ticket
    Authorization: Bearer <<TOKEN>>

    Response (simplified):

    json
    { "ticket": "abc123..." }

    The ticket is valid for about 30 seconds.

  2. The SPA opens a websocket using the ticket in the URL:

    ts
    const ws = new WebSocket("wss://<<URL>>/api/v1/events/" + ticket);
  3. If the ticket is valid and matches the token used to create it, the backend upgrades the connection and starts streaming events.

Recommendation
Keep the ticket request in the same place where you also obtain/refresh tokens. If your app uses refresh tokens, always create a new ticket after getting a new access token.

2.2 Backend / CLI: direct connection with header

For non-browser clients (Node.js services, Go/Java backends, CLI tools) it is often easier to send the Authorization header directly:

http
GET /api/v1/events
Authorization: Bearer <<TOKEN>>

How you attach headers depends on the websocket client library you use. The response status is 204 on success; the actual data comes in as websocket messages.

Use this variant if:

  • you fully control the HTTP layer (no browser restrictions), and
  • you already manage tokens in that process.

3. Message format and routing

Each websocket message is a JSON object describing a single event. The exact shape depends on the module that produced it, but all events have a type field, for example:

  • OBJECT_UPDATED (Service)
  • META_STATUS_CHANGE (Metadata)
  • CREATE_LIVESTREAM, BIN_STREAM_UPDATE (Streams)
  • ALARM_STATUS_ACTIVE (Alarms)
  • NOTE_MESSAGE_ADD (Notes)
  • IAM_KEY_ADD (IAM)

A simple event router in TypeScript might look like this:

ts
type ColdwaveEvent =
  | ServiceObjectUpdatedEvent
  | MetaStatusChangeEvent
  | StreamEvent
  | AlarmEvent
  | NoteEvent
  | IamEvent
  | HealthcheckEvent
  | { type: string; [key: string]: unknown }; // fallback

function handleEvent(evt: ColdwaveEvent) {
  switch (evt.type) {
    case "OBJECT_UPDATED":
      handleServiceUpdate(evt);
      break;
    case "META_STATUS_CHANGE":
    case "META_SET":
    case "META_DELETE":
      handleMetaEvent(evt);
      break;
    case "CREATE_LIVESTREAM":
    case "UPDATE_LIVESTREAM":
    case "DELETE_LIVESTREAM":
    case "BIN_STREAM_UPDATE":
    case "STRING_STREAM_UPDATE":
      handleStreamEvent(evt);
      break;
    case "ALARM_STATUS_ACTIVE":
    case "ALARM_STATUS_INACTIVE":
    case "ALARM_CONFIG_ADD":
    case "ALARM_CONFIG_UPDATE":
    case "ALARM_CONFIG_DELETE":
      handleAlarmEvent(evt);
      break;
    case "NOTE_MESSAGE_ADD":
    case "NOTE_MESSAGE_UPDATE":
    case "NOTE_MESSAGE_DELETE":
      handleNoteEvent(evt);
      break;
    case "IAM_KEY_ADD":
    case "IAM_KEY_UPDATE":
    case "IAM_KEY_DELETE":
    case "IAM_USER_CREATE":
    case "IAM_USER_UPDATE":
      handleIamEvent(evt);
      break;
    case "HEALTHCHECK_PONG":
      handleHealthcheck(evt);
      break;
    default:
      // Optional: log or ignore unknown event types
      break;
  }
}

In a real app you would:

  • Keep a single websocket connection for the whole app.
  • Parse messages in one central place (ws.onmessage).
  • Dispatch to module-specific slices in your state store (Redux, Vuex, Pinia…).

4. Healthcheck and connection lifecycle

4.1 Healthcheck ping/pong

The websocket module exposes a simple healthcheck mechanism:

  • Client sends a JSON message:

    ts
    ws.send(JSON.stringify({ type: "HEALTHCHECK_PING" }));
  • Backend responds with:

    json
    { "type": "HEALTHCHECK_PONG" }

You can use this to:

  • Confirm that the connection is still alive.
  • Measure latency (timestamp in payload is optional and can be added on the client side).

It is usually not needed for every app – the websocket close/error events already tell you if the connection broke – but it is useful for diagnostics and debugging.

4.2 Reconnect strategy

A robust reconnect strategy for SPAs/backends usually has these elements:

  1. On websocket close or error:

    • Wait for a small delay (e.g. 1–5 seconds, with exponential backoff).
    • Ensure the current access token is still valid (refresh if needed).
    • For browsers, request a new ticket with the fresh token.
    • Open a new websocket connection.
  2. After reconnect:

    • Optionally re-fetch critical state via REST (alarms, notes, active streams).
    • Continue handling events as before.

Avoid opening multiple websocket connections per tab – one global connection per app instance is usually enough.


5. Module-specific events and how to use them

This section summarizes the most important event types for application developers. The full and authoritative schemas are in the individual module docs; here we focus on how to use them in your state model and UI.

5.1 Service: OBJECT_UPDATED

From the Service module you receive OBJECT_UPDATED events whenever a service updates a property:

  • type: "OBJECT_UPDATED"
  • deviceIdentifier
  • serviceIdentifier
  • optional identifier (for multiple instances of the same service)
  • updates: array of property updates

You typically use this to:

  • Update the in-memory representation of a device/service in your state store.
  • Re-render dashboards, detail views, trends that depend on the latest values.

The updates array uses the same property representation (ID, type, modifiers) as the Service REST API. If you keep your state in the “schema-mapped” format, you can use the property identifier or schema name to apply the change.

Pattern:

  • On OBJECT_UPDATED:
    • Locate your cached device/service object.
    • Apply the property changes (value and any modifier flags).
    • If the service is not in cache yet, fetch it once via REST with depth.

5.2 Metadata: META_* events

From the Meta module you receive, among others:

  • META_STATUS_CHANGE – device online/offline/idle changes
  • META_SET / META_DELETE – user-defined metadata changes
  • META_SCHEMA_ADD / META_SCHEMA_UPDATE / META_SCHEMA_DELETE – meta schema changes

Typical uses:

  • Update a device’s status and lastMessage/lastConnect fields for “online indicator” badges.
  • Keep a local cache of metadata in sync when metadata is edited in another tab or by another user.
  • Live-refresh meta schema editors (admin UIs) when schemas are created or updated.

5.3 Streams: LIVESTREAM_* and *_STREAM_UPDATE

From the Stream module you receive:

  • CREATE_LIVESTREAM, UPDATE_LIVESTREAM, DELETE_LIVESTREAM
  • BIN_STREAM_UPDATE, STRING_STREAM_UPDATE

These events are tied to open property streams created via the /api/v1/streams API. Only the user who created a stream will receive the corresponding events.

Typical uses:

  • Realtime charts for fast-changing properties (e.g. speed, power consumption).
  • Log/trace views for string or binary streams.

Pattern:

  1. App calls POST /api/v1/streams for a given device/service/property.
  2. Backend acknowledges with a uuid and sends a CREATE_LIVESTREAM event.
  3. As data arrives, BIN_STREAM_UPDATE / STRING_STREAM_UPDATE events contain the samples.
  4. The app appends samples to an in-memory buffer or chart.
  5. When the view is closed, the app calls DELETE /api/v1/streams/:uuid and removes its local buffers.

5.4 Alarms: ALARM_CONFIG_* and ALARM_STATUS_*

From the Alarm module you receive events like:

  • ALARM_CONFIG_ADD, ALARM_CONFIG_UPDATE, ALARM_CONFIG_DELETE
  • ALARM_STATUS_ACTIVE, ALARM_STATUS_INACTIVE, ALARM_STATUS_DELETE_DEVICE

Typical uses:

  • Keep the list of alarm configurations in sync between multiple open UIs.
  • Update the “current alarm status” for each device without polling.
  • Show toast/notifications when an alarm becomes active.

Pattern:

  • Use REST to initially load alarm configs and status.
  • Subscribe to the events to update your alarm store incrementally.
  • For user-facing notifications, subscribe only to severe alarms (filter by severity on the backend or in your handler).

5.5 Notes: NOTE_MESSAGE_*

From the Notes module you receive:

  • NOTE_MESSAGE_ADD
  • NOTE_MESSAGE_UPDATE
  • NOTE_MESSAGE_DELETE

Typical uses:

  • Live collaboration on per-device notes.
  • Show “new note” indicators on device tiles or timeline views.

Pattern:

  • REST: load all notes for a device when its detail page opens.
  • Websocket: apply changes as events arrive and update the list in-place.

5.6 IAM: IAM_* events

From the IAM module you receive various IAM_* events, for example:

  • IAM_KEY_ADD, IAM_KEY_UPDATE, IAM_KEY_DELETE
  • IAM_GROUP_ADD, IAM_GROUP_UPDATE, IAM_GROUP_DELETE
  • IAM_ROLE_ADD, IAM_ROLE_UPDATE, IAM_ROLE_DELETE
  • IAM_USER_CREATE, IAM_USER_UPDATE, IAM_USER_DELETE, …

These are mainly relevant for admin / operator UIs:

  • Live update of user/role/group lists when multiple admins are active.
  • Refresh of API key lists when keys are created/rotated/deleted.

For a typical end-user portal or dashboard, you may not need to act on IAM events at all.


6. Permissions and scoping for events

Websocket events use the same IAM model as REST calls:

  • You only receive events for which your token has appropriate permissions:
    • e.g. read on service for OBJECT_UPDATED
    • read on meta for META_STATUS_CHANGE
    • read on stream for livestream events
    • read on alarms.status / alarms.config for alarm events
    • read on notes for notes events
    • read on iam.* for IAM events
  • Device and service scoping is applied:
    • If your token is restricted to certain deviceIdentifier or serviceIdentifier values, you will only see events for those devices/services.
    • Using wildcards (*) in IAM grants broader access and therefore more events.

For application developers this means:

  • If you “miss” expected events, check IAM roles and device/service scoping first, before debugging your websocket code.
  • You do not need to filter events by device/service yourself to enforce access control – this is already done in the backend.

7. Implementation patterns and pitfalls

7.1 One websocket per app

  • Open a single websocket connection per browser tab/app instance.
  • Use it as a “bus” and route messages to feature modules (services, meta, streams, alarms, notes, IAM).
  • Avoid opening a new connection for each component or feature – this wastes resources on both client and server.

7.2 Initial snapshot + incremental updates

  • On app startup or when entering a screen:
    • Use REST to fetch the initial state (devices, services, alarms, notes, …).
  • Use websocket events to:
    • update values,
    • add/remove entities,
    • and keep derived UI elements in sync.

Do not try to reconstruct full state purely from websocket events; REST remains the primary source of truth.

7.3 Token expiry and reconnect

  • Websocket connections do not automatically renew IAM tokens.
  • When your access token expires:
    • REST calls will start to fail with 401/403,
    • the websocket might also be closed by the backend.
  • Handle this by:
    • Watching token expiry server-side or via a timer,
    • Refreshing the token using the IAM refresh endpoint,
    • Closing and reopening the websocket with the new token/ticket.

7.4 Error handling and logging

  • Treat unknown type values in events as non-fatal – log them for debugging, but do not break the connection.
  • In development, log all events (or subsets) to the console to debug your routing logic.
  • In production, log only unexpected event types and connection errors.

With these patterns in place, the websocket module becomes the backbone of your live-updating UI, while the REST modules (Service, Meta, Streams, Timestream, Alarms, Notes, IAM) provide the initial data and on-demand queries.