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:

  1. A device has a web interface at 10.0.1.5:8080 (on the VPC)
  2. Your control app registers a proxy route: subdomain hvac -> http://10.0.1.5:8080
  3. Authenticated @sttark.com users can access it at https://hvac.eng.sttark.com with full SSL
  4. Programmatic clients can use X-API-Key header 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

  1. Track subscriptions locally - Store which devices you've subscribed to
  2. Resubscribe after reconnection - The broker forgets your subscriptions on restart
  3. Use exponential backoff - Prevents overwhelming the broker during recovery
  4. Add jitter - Prevents "thundering herd" when many clients reconnect simultaneously
  5. Detect reconnection - Check if connectionId was 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

  1. Store Connection ID: Keep the connectionId from the welcome message for debugging
  2. Handle Reconnection: Implement automatic reconnection and resubscribe to devices (see above)
  3. Track Device Status: Listen for online/offline events to update your UI
  4. Queue Commands: If a device is offline, queue commands or notify the user
  5. Error Handling: Always handle error messages gracefully
  6. Multiple Subscriptions: You can subscribe to multiple devices simultaneously
  7. Local Subscription Tracking: Keep a local Set of subscribed devices for reconnection
  8. Re-register Proxies: Proxy routes persist across server restarts, so you only need to register them once. Use list_proxies after connecting to check if your routes already exist before re-registering
  9. Monitor Proxy Health: Use check_proxy_health or listen on the /health WebSocket 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 devices
  • sub <deviceId> - Subscribe to a device
  • unsub <deviceId> - Unsubscribe from a device
  • send <deviceId> <json> - Send command to device
  • status - Show connection status
  • help - Show all commands
  • quit - 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.