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
- State-first: State is the source of truth, not events
- Deterministic: Same inputs produce same outputs
- Auditable: Every message is logged with correlation
- Resilient: Graceful degradation under failure
- 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
| Type | Description | Response Expected |
|---|---|---|
command | Request to perform an action | ack + optional event |
event | Notification of something that happened | None |
state | State update (delta or snapshot) | None |
ack | Acknowledgement of command receipt/completion | None |
error | Error response to a command or subscription | None |
subscribe | Request to subscribe to a topic pattern | ack |
unsubscribe | Request to unsubscribe from a topic pattern | ack |
source
Namespaced identifier for the message originator:
hub.core— The hub itselfcompanion.satellite— Companion satellite adapterbuttons.device— Buttons adapterapp.<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.myvarcompanion.page.1.bank.5.stylebuttons.device.abc123.key.7.stateapp.myapp.custom.statusReserved prefixes:
companion.*— Companion-related state and commandsbuttons.*— Buttons-related state and commandshub.*— Hub internal state and metricsapp.*— 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:
- Client sends
commandwithidempotencyKey - Hub validates and routes to target adapter
- Adapter sends
ackwith statusreceived - Adapter executes action
- Adapter sends
ackwith statuscompletedorfailed
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 valuevalue: null— Explicitly unset/clearedstale: 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:
| Code | Description |
|---|---|
INVALID_MESSAGE | Message failed schema validation |
UNKNOWN_TARGET | Target adapter/client not found |
TIMEOUT | Operation timed out |
RATE_LIMITED | Too many requests |
UNAUTHORIZED | Missing or invalid auth |
FORBIDDEN | Action not permitted |
ADAPTER_ERROR | Adapter-specific failure |
STATE_CONFLICT | State 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 variablescompanion.page.1.**— Page 1, all banks, all propertiesbuttons.device.*.key.*— All keys on all devices
Wildcards:
*— Single level (e.g.,a.*.cmatchesa.b.cbut nota.b.d.c)**— Multi-level (e.g.,a.**matchesa.b,a.b.c,a.b.c.d)
Subscribe Response
On successful subscription, the hub sends:
ackwith subscription confirmation- If
snapshot: true, current state for all matching keys - 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.satellitebuttons.device.<id>.* → owned by buttons.deviceapp.<name>.custom.* → owned by app.<name>hub.metrics.* → owned by hub.coreStaleness
When an adapter disconnects:
- All state keys owned by that adapter are marked
stale: true - Subscribers receive state updates with
stale: true - 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
- Client connects to hub
- Client sends
subscribewithsnapshot: true - Hub sends
ackconfirming subscription - Hub sends batch of
statemessages for all matching keys - Hub sends
eventwithevent: 'snapshot_complete' - Hub sends deltas as state changes occur
Delta Updates
After snapshot, clients receive only changes:
- New/updated keys:
statemessage with new value - Deleted keys:
statemessage withvalue: null - Stale keys:
statemessage withstale: true
Idempotency
Commands use idempotencyKey for deduplication:
- Hub maintains registry of recently processed keys
- Duplicate key within TTL returns cached result
- Registry entries expire after 2× TTL
- 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.jsonlFormat: 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:
- Feeds messages into a simulator environment
- Maintains correct timing relationships
- 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
| Version | Date | Changes |
|---|---|---|
| 0.1 | 2024-01-17 | Initial draft |