Skip to content

Bridge protocol

Canonical protocol for the Bitfocus Telemetry Bridge message exchange.

Overview

The Bridge Protocol defines a vendor-agnostic message format for state synchronisation, command execution, and event propagation between the telemetry hub and connected clients.

Design Principles

  1. State-first: State is the source of truth, not events
  2. Deterministic: Same inputs produce same outputs
  3. Auditable: Every message is logged with correlation
  4. Resilient: Graceful degradation under failure
  5. Vendor-agnostic: Core protocol has no Companion/Buttons specifics

Message Envelope

Every message conforms to this envelope structure:

interface BridgeMessage {
/** Unique message identifier (UUID v7, time-ordered) */
id: string;
/** Message type discriminator */
type: 'command' | 'event' | 'state' | 'ack' | 'error' | 'subscribe' | 'unsubscribe';
/** Source namespace (who sent this) */
source: string;
/** Target namespace (for commands/acks) */
target?: string;
/** Hierarchical path for routing and state keys */
path: string;
/** Type-dependent payload */
payload: unknown;
/** Unix timestamp in milliseconds */
timestamp: number;
/** Links related messages across the system */
correlationId?: string;
/** Monotonically increasing per source stream */
sequence: number;
/** Time-to-live in milliseconds (commands only) */
ttl?: number;
/** Idempotency key for command deduplication */
idempotencyKey?: string;
}

Field Semantics

id

  • UUID v7 format (RFC 9562)
  • Time-ordered for natural chronological sorting
  • Generated by the message originator

type

TypeDescriptionResponse Expected
commandRequest to perform an actionack + optional event
eventNotification of something that happenedNone
stateState update (delta or snapshot)None
ackAcknowledgement of command receipt/completionNone
errorError response to a command or subscriptionNone
subscribeRequest to subscribe to a topic patternack
unsubscribeRequest to unsubscribe from a topic patternack

source

Namespaced identifier for the message originator:

  • hub.core — The hub itself
  • companion.satellite — Companion satellite adapter
  • buttons.device — Buttons adapter
  • app.<name> — Client application with registered name

target

For directed messages (commands, acks, errors), specifies the recipient. Omit for broadcast messages (events, state updates).

path

Hierarchical addressing using dot notation:

companion.variables.internal.myvar
companion.page.1.bank.5.style
buttons.device.abc123.key.7.state
app.myapp.custom.status

Reserved prefixes:

  • companion.* — Companion-related state and commands
  • buttons.* — Buttons-related state and commands
  • hub.* — Hub internal state and metrics
  • app.* — Application-specific state

sequence

  • Monotonically increasing integer per source
  • Enables ordering and gap detection
  • Resets only on source restart (clients should handle)

Message Types

Commands

Commands request the hub or an adapter to perform an action.

interface CommandMessage extends BridgeMessage {
type: 'command';
target: string;
payload: {
action: string;
params?: Record<string, unknown>;
};
ttl?: number;
idempotencyKey: string;
}

Example: Press a Companion button

{
"id": "019449a0-1234-7def-8abc-def012345678",
"type": "command",
"source": "app.myapp",
"target": "companion.satellite",
"path": "companion.page.1.bank.5",
"payload": {
"action": "press"
},
"timestamp": 1705488000000,
"correlationId": "req-abc-123",
"sequence": 42,
"ttl": 5000,
"idempotencyKey": "press-1-5-1705488000"
}

Command Lifecycle:

  1. Client sends command with idempotencyKey
  2. Hub validates and routes to target adapter
  3. Adapter sends ack with status received
  4. Adapter executes action
  5. Adapter sends ack with status completed or failed

Events

Events notify subscribers of something that happened. Fire-and-forget semantics.

interface EventMessage extends BridgeMessage {
type: 'event';
payload: {
event: string;
data?: unknown;
};
}

Example: Button pressed notification

{
"id": "019449a0-5678-7def-8abc-def012345679",
"type": "event",
"source": "companion.satellite",
"path": "companion.page.1.bank.5",
"payload": {
"event": "pressed",
"data": {
"page": 1,
"bank": 5,
"duration": null
}
},
"timestamp": 1705488000100,
"sequence": 1001
}

State Updates

State messages carry the current value of a state key.

interface StateMessage extends BridgeMessage {
type: 'state';
payload: {
value: unknown;
stale?: boolean;
owner?: string;
version?: number;
};
}

State Value Semantics:

  • value: <any> — Current value
  • value: null — Explicitly unset/cleared
  • stale: true — Value may be outdated (source disconnected)
  • owner — Namespace that owns this state key

Example: Variable update

{
"id": "019449a0-9abc-7def-8abc-def012345680",
"type": "state",
"source": "companion.satellite",
"path": "companion.variables.internal.tally_pgm",
"payload": {
"value": "Camera 1",
"owner": "companion.satellite",
"version": 5
},
"timestamp": 1705488000200,
"sequence": 1002
}

Acknowledgements

Acks confirm command receipt and completion status.

interface AckMessage extends BridgeMessage {
type: 'ack';
payload: {
status: 'received' | 'completed' | 'failed' | 'timeout' | 'rejected';
commandId: string;
result?: unknown;
error?: {
code: string;
message: string;
details?: unknown;
};
};
}

Example: Command completed

{
"id": "019449a0-def0-7def-8abc-def012345681",
"type": "ack",
"source": "companion.satellite",
"target": "app.myapp",
"path": "companion.page.1.bank.5",
"payload": {
"status": "completed",
"commandId": "019449a0-1234-7def-8abc-def012345678"
},
"timestamp": 1705488000300,
"correlationId": "req-abc-123",
"sequence": 1003
}

Errors

Error messages indicate failures in processing.

interface ErrorMessage extends BridgeMessage {
type: 'error';
payload: {
code: string;
message: string;
details?: unknown;
relatedMessageId?: string;
};
}

Standard Error Codes:

CodeDescription
INVALID_MESSAGEMessage failed schema validation
UNKNOWN_TARGETTarget adapter/client not found
TIMEOUTOperation timed out
RATE_LIMITEDToo many requests
UNAUTHORIZEDMissing or invalid auth
FORBIDDENAction not permitted
ADAPTER_ERRORAdapter-specific failure
STATE_CONFLICTState write conflict

Subscriptions

Clients subscribe to receive state updates and events matching a topic pattern.

Subscribe Request

interface SubscribeMessage extends BridgeMessage {
type: 'subscribe';
payload: {
patterns: string[];
filter?: 'state' | 'events' | 'all';
snapshot?: boolean;
};
}

Pattern Syntax:

  • companion.variables.* — All Companion variables
  • companion.page.1.** — Page 1, all banks, all properties
  • buttons.device.*.key.* — All keys on all devices

Wildcards:

  • * — Single level (e.g., a.*.c matches a.b.c but not a.b.d.c)
  • ** — Multi-level (e.g., a.** matches a.b, a.b.c, a.b.c.d)

Subscribe Response

On successful subscription, the hub sends:

  1. ack with subscription confirmation
  2. If snapshot: true, current state for all matching keys
  3. Ongoing deltas for matching state changes

State Store Semantics

Ownership

Each state key has a single owner (namespace). Only the owner may update the key.

companion.variables.* → owned by companion.satellite
buttons.device.<id>.* → owned by buttons.device
app.<name>.custom.* → owned by app.<name>
hub.metrics.* → owned by hub.core

Staleness

When an adapter disconnects:

  1. All state keys owned by that adapter are marked stale: true
  2. Subscribers receive state updates with stale: true
  3. On reconnect, adapter republishes current state (stale flag cleared)

Versioning

Each state key maintains a monotonic version number:

  • Incremented on every change
  • Used for conflict detection
  • Enables efficient delta comparison

Snapshot and Delta Protocol

Initial Connection

  1. Client connects to hub
  2. Client sends subscribe with snapshot: true
  3. Hub sends ack confirming subscription
  4. Hub sends batch of state messages for all matching keys
  5. Hub sends event with event: 'snapshot_complete'
  6. Hub sends deltas as state changes occur

Delta Updates

After snapshot, clients receive only changes:

  • New/updated keys: state message with new value
  • Deleted keys: state message with value: null
  • Stale keys: state message with stale: true

Idempotency

Commands use idempotencyKey for deduplication:

  1. Hub maintains registry of recently processed keys
  2. Duplicate key within TTL returns cached result
  3. Registry entries expire after 2× TTL
  4. Keys should be deterministic (e.g., include timestamp + action + params hash)

Backpressure

Client-side

Clients should implement:

  • Message batching when sending many commands
  • Rate limiting (configurable, default 100 msg/s)
  • Queue with bounded size (drop oldest or reject new)

Hub-side

Hub implements:

  • Per-client rate limiting
  • Subscription throttling (coalesce rapid state changes)
  • Slow consumer detection and disconnection

Event Log Format

All messages are appended to a rotating log file for replay:

data/events/2024-01-17.jsonl

Format: JSON Lines (one message per line)

{"id":"...","type":"command",...,"_logged":1705488000000}
{"id":"...","type":"ack",...,"_logged":1705488000100}

The _logged field is added by the hub (not part of the message schema).

Replay

The replay tool reads log files and:

  1. Feeds messages into a simulator environment
  2. Maintains correct timing relationships
  3. Enables debugging without live systems

Security Considerations

Authentication

  • Optional bearer token in WebSocket upgrade request
  • Token validation on connect
  • No per-message auth (connection-level only)

Authorisation

  • Clients can only write to state keys they own
  • Clients can only send commands to registered targets
  • Wildcard subscriptions may be restricted by policy

Transport

  • WebSocket connections only (no raw TCP)
  • TLS recommended for production
  • No secrets in message payloads or paths

Appendix: JSON Schema

See /config/schemas/bridge-message.schema.json for the complete JSON Schema definition of the message envelope.


Version History

VersionDateChanges
0.12024-01-17Initial draft