Control App Integration Guide
This guide explains how to build a control application (web app, dashboard, mobile app) that connects to the Sttark Machine Broker to monitor and control devices.
Overview
┌─────────────────┐ ┌─────────────────┐
│ │ 1. Connect │ │
│ Control App │ ───────────────► │ Broker │
│ (Web/Mobile) │ 2. Subscribe │ │
│ │ ◄───────────────►│ /control │
│ │ 3. Send/Recv │ │
└─────────────────┘ └─────────────────┘
Connection Flow
Step 1: Establish WebSocket Connection
Connect to the broker's /control endpoint with your API key.
Endpoint: wss://machinebroker.sttark.com/control
Authentication: Provide API key via query parameter or header:
- Query:
wss://machinebroker.sttark.com/control?apiKey=YOUR_API_KEY - Header:
X-API-Key: YOUR_API_KEY
// REQUIRED: Connect with API key
const ws = new WebSocket(
"wss://machinebroker.sttark.com/control?apiKey=YOUR_API_KEY"
);
Step 2: Receive Welcome Message
Upon connection, the broker sends a welcome message with your connection ID:
{
"type": "welcome",
"connectionId": "550e8400-e29b-41d4-a716-446655440000",
"message": "Connected to control endpoint. Send subscribe message with deviceId to receive device messages.",
"timestamp": "2024-01-15T10:30:00.000Z"
}
Step 3: List Available Devices (Optional)
Before subscribing, you can request a list of connected devices:
Send:
{
"type": "list_devices"
}
Receive:
{
"type": "success",
"action": "list_devices",
"devices": [
{
"deviceId": "sensor-001",
"connectedAt": "2024-01-15T10:25:00.000Z",
"metadata": {
"name": "Temperature Sensor",
"label": "Kitchen Sensor",
"type": "sensor"
},
"subscriberCount": 2
},
{
"deviceId": "actuator-002",
"connectedAt": "2024-01-15T10:26:00.000Z",
"metadata": {
"name": "Smart Thermostat",
"type": "actuator"
},
"subscriberCount": 1
}
],
"timestamp": "2024-01-15T10:30:01.000Z"
}
Step 4: Subscribe to Devices
Subscribe to receive messages from specific devices:
Send:
{
"type": "subscribe",
"deviceId": "sensor-001"
}
| Field | Required | Description |
|---|---|---|
type |
Yes | Must be "subscribe" |
deviceId |
Yes | The device ID to subscribe to |
Receive:
{
"type": "success",
"action": "subscribed",
"deviceId": "sensor-001",
"deviceOnline": true,
"timestamp": "2024-01-15T10:30:02.000Z"
}
Note: You can subscribe to a device even if it's offline. You'll receive notifications when it comes online.
Step 5: Receive Device Messages
After subscribing, you'll receive messages from the device:
{
"type": "message",
"deviceId": "sensor-001",
"payload": {
"temperature": 23.5,
"humidity": 65,
"status": "ok"
},
"timestamp": "2024-01-15T10:30:05.000Z"
}
Step 6: Receive Device Status Updates
You'll also receive notifications when subscribed devices connect or disconnect:
Device came online:
{
"type": "device_online",
"deviceId": "sensor-001",
"timestamp": "2024-01-15T10:30:00.000Z"
}
Device went offline:
{
"type": "device_offline",
"deviceId": "sensor-001",
"timestamp": "2024-01-15T10:35:00.000Z"
}
Device reconnecting (same ID, new connection):
{
"type": "device_reconnecting",
"deviceId": "sensor-001",
"timestamp": "2024-01-15T10:36:00.000Z"
}
Step 7: Send Commands to Devices
Send commands to a specific device:
Send:
{
"type": "message",
"deviceId": "actuator-002",
"payload": {
"command": "set_temperature",
"value": 25
}
}
| Field | Required | Description |
|---|---|---|
type |
Yes | Must be "message" |
deviceId |
Yes | Target device ID |
payload |
Yes | Command data (any JSON object) |
Error if device not connected:
{
"type": "error",
"error": "Device actuator-002 is not connected",
"deviceId": "actuator-002",
"timestamp": "2024-01-15T10:31:00.000Z"
}
Step 8: Unsubscribe from Devices
Stop receiving messages from a device:
Send:
{
"type": "unsubscribe",
"deviceId": "sensor-001"
}
Receive:
{
"type": "success",
"action": "unsubscribed",
"deviceId": "sensor-001",
"timestamp": "2024-01-15T10:32:00.000Z"
}
Dynamic Proxy Management
Control apps can register dynamic reverse proxy routes. This allows you to expose a device's web interface (e.g., an HVAC controller's admin panel) through a public subdomain with SSL, without managing individual certificates or DNS records.
How it works:
- A device has a web interface at
10.0.1.5:8080(on the VPC) - Your control app registers a proxy route: subdomain
hvac->http://10.0.1.5:8080 - Authenticated
@sttark.comusers can access it athttps://hvac.eng.sttark.comwith full SSL - Programmatic clients can use
X-API-Keyheader to bypass Google OAuth
Register a Proxy Route
Send:
{
"type": "register_proxy",
"subdomain": "hvac",
"target": "http://192.168.1.50:8080"
}
| Field | Required | Description |
|---|---|---|
type |
Yes | Must be "register_proxy" |
subdomain |
Yes | The subdomain to register (e.g., "hvac" becomes hvac.eng.sttark.com) |
target |
Yes | The target URL to proxy to (IP, domain, with port) |
Receive:
{
"type": "proxy_registered",
"action": "proxy_registered",
"subdomain": "hvac",
"target": "http://192.168.1.50:8080",
"url": "https://hvac.eng.sttark.com",
"replaced": false,
"timestamp": "2026-02-05T10:30:00.000Z"
}
Target URL formats:
// IP with port
{ "target": "http://192.168.1.50:8080" }
// Domain name
{ "target": "http://internal-server.local" }
// HTTPS target
{ "target": "https://secure-backend.example.com" }
// Without protocol (defaults to http://)
{ "target": "192.168.1.50:8080" }
Subdomain rules:
- Lowercase alphanumeric and hyphens only
- Cannot start or end with a hyphen
- Reserved names:
www,api,mail,ftp,admin,broker
Remove a Proxy Route
Send:
{
"type": "unregister_proxy",
"subdomain": "hvac"
}
Receive:
{
"type": "proxy_unregistered",
"action": "proxy_unregistered",
"subdomain": "hvac",
"timestamp": "2026-02-05T10:35:00.000Z"
}
List All Proxy Routes
Send:
{
"type": "list_proxies"
}
Receive:
{
"type": "success",
"action": "list_proxies",
"proxies": [
{
"subdomain": "hvac",
"target": "http://192.168.1.50:8080",
"url": "https://hvac.eng.sttark.com",
"health": {
"status": "healthy",
"lastCheck": "2026-02-05T10:30:00.000Z",
"responseTimeMs": 45,
"consecutiveFailures": 0,
"errorMessage": null
},
"stats": {
"requestCount": 1234,
"lastAccessed": "2026-02-05T10:29:55.000Z",
"createdAt": "2026-02-05T08:00:00.000Z"
}
}
],
"summary": {
"total": 1,
"healthy": 1,
"unhealthy": 0,
"unknown": 0
},
"timestamp": "2026-02-05T10:30:01.000Z"
}
Check Proxy Health
Trigger an immediate health check for a specific proxy or all proxies:
Send (specific):
{
"type": "check_proxy_health",
"subdomain": "hvac"
}
Send (all):
{
"type": "check_proxy_health"
}
Receive:
{
"type": "success",
"action": "check_proxy_health",
"message": "Health check triggered for hvac",
"timestamp": "2026-02-05T10:30:05.000Z"
}
Health results are broadcast asynchronously to the /health WebSocket endpoint.
Health Status Values
| Status | Meaning |
|---|---|
healthy |
Target responded with 2xx/3xx status within timeout |
unhealthy |
Target failed 3+ consecutive health checks |
unknown |
Route was recently registered, not yet checked |
Complete Message Reference
Messages You Send
| Message Type | Purpose | Required Fields |
|---|---|---|
list_devices |
Get all connected devices | None |
subscribe |
Subscribe to a device | deviceId |
unsubscribe |
Unsubscribe from a device | deviceId |
message |
Send command to device | deviceId, payload |
register_proxy |
Register a proxy subdomain | subdomain, target |
unregister_proxy |
Remove a proxy route | subdomain |
list_proxies |
List all proxy routes | None |
check_proxy_health |
Trigger proxy health check | subdomain (optional) |
Messages You Receive
| Message Type | Description |
|---|---|
welcome |
Sent on connection, includes your connectionId |
success |
Action completed successfully |
error |
Something went wrong |
message |
Data from a subscribed device |
device_online |
Subscribed device connected |
device_offline |
Subscribed device disconnected |
device_reconnecting |
Subscribed device is reconnecting |
proxy_registered |
Proxy route was registered successfully |
proxy_unregistered |
Proxy route was removed successfully |
Example Implementation
See test/mock-control.js for a complete Node.js example. Here's a minimal implementation:
const WebSocket = require("ws");
// Configuration
const BROKER_URL = "wss://machinebroker.sttark.com/control";
const API_KEY = "your-api-key";
// State
let connectionId = null;
const subscribedDevices = new Set();
// Connect
const ws = new WebSocket(`${BROKER_URL}?apiKey=${API_KEY}`);
ws.on("open", () => {
console.log("Connected to broker");
});
ws.on("message", (data) => {
const message = JSON.parse(data);
switch (message.type) {
case "welcome":
connectionId = message.connectionId;
console.log("Connection ID:", connectionId);
// Now you can list devices or subscribe
listDevices();
break;
case "success":
handleSuccess(message);
break;
case "message":
// Data from a device
console.log(`Device ${message.deviceId}:`, message.payload);
handleDeviceData(message.deviceId, message.payload);
break;
case "device_online":
console.log(`Device ${message.deviceId} is now online`);
break;
case "device_offline":
console.log(`Device ${message.deviceId} went offline`);
break;
case "proxy_registered":
console.log(`Proxy ready: ${message.url}`);
break;
case "error":
console.error("Error:", message.error);
break;
}
});
// List all connected devices
function listDevices() {
ws.send(JSON.stringify({ type: "list_devices" }));
}
// Subscribe to a device
function subscribeToDevice(deviceId) {
ws.send(
JSON.stringify({
type: "subscribe",
deviceId: deviceId,
})
);
subscribedDevices.add(deviceId);
}
// Send command to a device
function sendCommand(deviceId, command) {
ws.send(
JSON.stringify({
type: "message",
deviceId: deviceId,
payload: command,
})
);
}
// Register a proxy route for a device's web interface
function registerProxy(subdomain, targetIp, targetPort) {
ws.send(
JSON.stringify({
type: "register_proxy",
subdomain: subdomain,
target: `http://${targetIp}:${targetPort}`,
})
);
}
// Handle success responses
function handleSuccess(message) {
switch (message.action) {
case "list_devices":
console.log("Available devices:", message.devices);
// Subscribe to first device as example
if (message.devices.length > 0) {
subscribeToDevice(message.devices[0].deviceId);
}
break;
case "subscribed":
console.log(
`Subscribed to ${message.deviceId} (online: ${message.deviceOnline})`
);
break;
case "list_proxies":
console.log("Proxy routes:", message.proxies);
console.log("Health summary:", message.summary);
break;
}
}
// Handle incoming device data
function handleDeviceData(deviceId, payload) {
// Your application logic here
// e.g., update UI, store in database, trigger actions
}
Proxy-Focused Example
Here's an example that registers proxy routes for devices that have web interfaces:
const WebSocket = require("ws");
const BROKER_URL = "wss://machinebroker.sttark.com/control";
const API_KEY = "your-api-key";
// Map of device IDs to their web interface config
const deviceInterfaces = {
"hvac-controller-01": { subdomain: "hvac", ip: "192.168.1.50", port: 8080 },
"plc-building-a": { subdomain: "plc-a", ip: "10.0.0.5", port: 80 },
"camera-lobby": { subdomain: "cam-lobby", ip: "192.168.1.100", port: 443 },
};
const ws = new WebSocket(`${BROKER_URL}?apiKey=${API_KEY}`);
ws.on("open", () => {
console.log("Connected to broker");
});
ws.on("message", (data) => {
const message = JSON.parse(data);
switch (message.type) {
case "welcome":
console.log("Connection ID:", message.connectionId);
// Register all proxy routes on connect
for (const [deviceId, config] of Object.entries(deviceInterfaces)) {
ws.send(JSON.stringify({
type: "register_proxy",
subdomain: config.subdomain,
target: `http://${config.ip}:${config.port}`,
}));
}
// List existing proxies
ws.send(JSON.stringify({ type: "list_proxies" }));
break;
case "proxy_registered":
console.log(`Proxy active: ${message.url} -> ${message.target}`);
// Output: Proxy active: https://hvac.eng.sttark.com -> http://192.168.1.50:8080
break;
case "success":
if (message.action === "list_proxies") {
console.log("\nProxy Routes:");
for (const proxy of message.proxies) {
const health = proxy.health.status;
const icon = health === "healthy" ? "[OK]" : health === "unhealthy" ? "[!!]" : "[??]";
console.log(` ${icon} ${proxy.url} -> ${proxy.target} (${health})`);
}
console.log(`\nTotal: ${message.summary.total} | Healthy: ${message.summary.healthy} | Unhealthy: ${message.summary.unhealthy}`);
}
break;
case "error":
console.error("Error:", message.error);
break;
}
});
Output:
Connected to broker
Connection ID: 550e8400-e29b-41d4-a716-446655440000
Proxy active: https://hvac.eng.sttark.com -> http://192.168.1.50:8080
Proxy active: https://plc-a.eng.sttark.com -> http://10.0.0.5:80
Proxy active: https://cam-lobby.eng.sttark.com -> http://192.168.1.100:443
Proxy Routes:
[OK] https://hvac.eng.sttark.com -> http://192.168.1.50:8080 (healthy)
[!!] https://plc-a.eng.sttark.com -> http://10.0.0.5:80 (unhealthy)
[??] https://cam-lobby.eng.sttark.com -> http://192.168.1.100:443 (unknown)
Total: 3 | Healthy: 1 | Unhealthy: 1
Connection Lifecycle
┌──────────────────────────────────────────────────────────────┐
│ Control App Connection Lifecycle │
├──────────────────────────────────────────────────────────────┤
│ │
│ 1. CONNECT ──► WebSocket to /control?apiKey=... │
│ │ │
│ ▼ │
│ 2. RECEIVE ◄── Welcome message (with connectionId) │
│ │ │
│ ▼ │
│ 3. SEND ────► list_devices (optional) │
│ │ │
│ ▼ │
│ 4. SEND ────► subscribe to device(s) │
│ │ │
│ ├────► register_proxy (optional, for web interfaces) │
│ │ │
│ ▼ │
│ 5. LOOP ◄──── Receive device data │
│ │ ◄── Receive device status changes │
│ │ ──► Send commands to devices │
│ │ ──► list_proxies / check_proxy_health │
│ │ │
│ ▼ │
│ 6. CLOSE ───► Disconnect when done │
│ │
└──────────────────────────────────────────────────────────────┘
Note: Proxy routes persist across server restarts. You only need to register them once. Subscriptions, however, are cleared on disconnect and must be re-established.
Web Browser Example
<!DOCTYPE html>
<html>
<head>
<title>Device Monitor</title>
</head>
<body>
<div id="devices"></div>
<div id="data"></div>
<script>
const API_KEY = "your-api-key";
const ws = new WebSocket(
`wss://machinebroker.sttark.com/control?apiKey=${API_KEY}`
);
ws.onopen = () => {
console.log("Connected");
};
ws.onmessage = (event) => {
const message = JSON.parse(event.data);
if (message.type === "welcome") {
// Request device list
ws.send(JSON.stringify({ type: "list_devices" }));
}
if (message.type === "success" && message.action === "list_devices") {
// Display devices and subscribe to all
message.devices.forEach((device) => {
ws.send(
JSON.stringify({
type: "subscribe",
deviceId: device.deviceId,
})
);
});
}
if (message.type === "message") {
// Display device data
document.getElementById("data").innerHTML += `<p><strong>${
message.deviceId
}:</strong> ${JSON.stringify(message.payload)}</p>`;
}
};
// Send command to device
function sendCommand(deviceId, command) {
ws.send(
JSON.stringify({
type: "message",
deviceId: deviceId,
payload: command,
})
);
}
</script>
</body>
</html>
Reconnection Strategy
Important: The broker does not persist connection state. When the broker restarts (e.g., during deployments), all connections are dropped. Control apps must reconnect and resubscribe to devices.
Recommended Approach: Exponential Backoff with Auto-Resubscribe
const RECONNECT_BASE_DELAY = 1000; // Start with 1 second
const RECONNECT_MAX_DELAY = 30000; // Cap at 30 seconds
let reconnectAttempts = 0;
let isIntentionalClose = false;
let connectionId = null;
const subscribedDevices = new Set(); // Track subscriptions locally
function getReconnectDelay() {
const delay = Math.min(
RECONNECT_BASE_DELAY * Math.pow(2, reconnectAttempts),
RECONNECT_MAX_DELAY
);
// Add jitter (±20%) to prevent thundering herd
const jitter = delay * 0.2 * (Math.random() - 0.5);
return Math.round(delay + jitter);
}
function resubscribeToDevices(ws) {
console.log(`Resubscribing to ${subscribedDevices.size} device(s)...`);
for (const deviceId of subscribedDevices) {
ws.send(JSON.stringify({ type: "subscribe", deviceId }));
}
}
function connect() {
const ws = new WebSocket(`${BROKER_URL}?apiKey=${API_KEY}`);
ws.on("open", () => {
console.log("Connected");
reconnectAttempts = 0; // Reset on success
});
ws.on("message", (data) => {
const message = JSON.parse(data);
if (message.type === "welcome") {
const wasReconnect = connectionId !== null;
connectionId = message.connectionId;
// IMPORTANT: Resubscribe after reconnection
if (wasReconnect && subscribedDevices.size > 0) {
resubscribeToDevices(ws);
}
}
// Handle other messages...
});
ws.on("close", (code) => {
if (!isIntentionalClose) {
const delay = getReconnectDelay();
reconnectAttempts++;
console.log(`Reconnecting in ${delay}ms (attempt ${reconnectAttempts})...`);
setTimeout(connect, delay);
}
});
ws.on("error", (err) => {
console.error("WebSocket error:", err.message);
// Let the close handler trigger reconnection
});
return ws;
}
// Subscribe to a device (tracks locally for reconnection)
function subscribeToDevice(ws, deviceId) {
subscribedDevices.add(deviceId);
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: "subscribe", deviceId }));
}
}
// Start connection
const ws = connect();
Key Points
- Track subscriptions locally - Store which devices you've subscribed to
- Resubscribe after reconnection - The broker forgets your subscriptions on restart
- Use exponential backoff - Prevents overwhelming the broker during recovery
- Add jitter - Prevents "thundering herd" when many clients reconnect simultaneously
- Detect reconnection - Check if
connectionIdwas previously set to know if this is a reconnect
Connection Close Codes
| Code | Meaning | Should Reconnect? |
|---|---|---|
| 1000 | Normal closure | Only if unintentional |
| 1001 | Going away (server shutdown) | Yes |
| 1006 | Abnormal closure | Yes |
| 1011 | Server error | Yes (with backoff) |
| 1012 | Service restart | Yes |
Browser-Specific Considerations
For web apps, also handle page visibility changes:
// Reconnect when page becomes visible again
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "visible" && ws.readyState !== WebSocket.OPEN) {
connect();
}
});
// Handle browser going offline/online
window.addEventListener("online", () => {
if (ws.readyState !== WebSocket.OPEN) {
connect();
}
});
Best Practices
- Store Connection ID: Keep the connectionId from the welcome message for debugging
- Handle Reconnection: Implement automatic reconnection and resubscribe to devices (see above)
- Track Device Status: Listen for online/offline events to update your UI
- Queue Commands: If a device is offline, queue commands or notify the user
- Error Handling: Always handle error messages gracefully
- Multiple Subscriptions: You can subscribe to multiple devices simultaneously
- Local Subscription Tracking: Keep a local Set of subscribed devices for reconnection
- Re-register Proxies: Proxy routes persist across server restarts, so you only need to register them once. Use
list_proxiesafter connecting to check if your routes already exist before re-registering - Monitor Proxy Health: Use
check_proxy_healthor listen on the/healthWebSocket for real-time health status of your proxy targets
REST API for Proxy Management
In addition to WebSocket messages, proxy routes can be queried via REST. All endpoints require authentication via X-API-Key header.
# List all proxy routes with health status
curl -H "X-API-Key: YOUR_API_KEY" \
https://machinebroker.sttark.com/api/proxies
# Get details for a specific proxy
curl -H "X-API-Key: YOUR_API_KEY" \
https://machinebroker.sttark.com/api/proxies/hvac
# Get health details for a proxy
curl -H "X-API-Key: YOUR_API_KEY" \
https://machinebroker.sttark.com/api/proxies/hvac/health
# Trigger an immediate health check
curl -X POST -H "X-API-Key: YOUR_API_KEY" \
https://machinebroker.sttark.com/api/proxies/hvac/check
Testing Your Integration
Use the mock control script to test:
# Set your API key
export API_KEY=your-api-key
# Run mock control app
node test/mock-control.js wss://machinebroker.sttark.com
Interactive commands:
list- List all connected devicessub <deviceId>- Subscribe to a deviceunsub <deviceId>- Unsubscribe from a devicesend <deviceId> <json>- Send command to devicestatus- Show connection statushelp- Show all commandsquit- Disconnect
Testing Proxy Routes
You can test proxy registration from any WebSocket client:
# Using wscat (install with: npm install -g wscat)
wscat -c "wss://machinebroker.sttark.com/control?apiKey=YOUR_API_KEY"
# Then send:
{"type": "register_proxy", "subdomain": "test", "target": "http://httpbin.org"}
{"type": "list_proxies"}
{"type": "check_proxy_health", "subdomain": "test"}
{"type": "unregister_proxy", "subdomain": "test"}
After registering, open https://test.eng.sttark.com in a browser to verify the proxy is working.