Subscribe to live changes and device-control events over Supabase Realtime at wss://api.busymate.net/realtime/v1 — the same socket the dashboard and the iOS app use to stay in sync without polling.
Realtime is authenticated by the same OAuth access token you use for REST and MCP. The token's role gates which channels you may join, so you only ever receive events you're allowed to see.
Two delivery modes
Busymate uses both of Realtime's mechanisms:
- Broadcast — named channels carry small, purpose-built control + firehose messages. This is the primary path: the row-level auth check happens once at channel-join time, so it scales to a high insert rate.
postgres_changes— table-level change data capture (CDC). Subscribe toINSERT/UPDATE/DELETEon a specific table and receive the changed row. Useful when you want to mirror a table's state.
Connecting
Use @supabase/supabase-js (or any client that speaks the Realtime protocol). Point it at the Busymate host and attach your OAuth token:
import { createClient } from "@supabase/supabase-js";
const supabase = createClient("https://api.busymate.net", PUBLISHABLE_KEY, {
global: { headers: { Authorization: `Bearer ${TOKEN}` } },
});The client opens wss://api.busymate.net/realtime/v1 under the hood; you work with channels.
Broadcast channels
Each channel carries a fixed set of named events. The table below marks who each channel is for — channels meant for integrators vs. ones that exist to drive a specific Busymate client (you can observe some of them, but they're designed for that client).
| Channel | Carries | For |
|---|---|---|
ws:<workspace_id> | The capture firehose for a workspace — new captured entries and breakpoint (paused-request) events, fanned out as broadcasts. | Integrators building a live feed; this is what the dashboard subscribes to. |
device:<uuid> | Per-device control + results — see the device events below. | The owning device + tools that drive it. |
devices:all | Fleet changes — a device added, renamed, or removed. | Dashboard / any fleet view. |
service_groups:all | A service group (Proxyman-style "App") was created, edited, or deleted. | Dashboard / settings views. |
settings:global | A coarse "global settings changed" invalidation — re-fetch your effective settings. | Every device. |
settings_device:all | Per-device settings overrides changed. | Dashboard / settings views. |
proxy-control | Breakpoint-continue and resend-request commands. | The proxy-server + iOS (control plane, not for integrators). |
device:<uuid> events
The per-device channel multiplexes several control flows. The topic name is literally device: followed by the device's uuid (for example device:6f1c…):
| Event | Direction | Payload | Meaning |
|---|---|---|---|
unpair | → device | — | Clear the device's stored credential; it returns to its activation gate. |
vpn-on / vpn-off | → device | — | Ask the device to connect / disconnect its VPN tunnel. |
open-sheet | → device | { sheet, open } (sheet ∈ settings | cert | pac) | Remotely open or close one of the iOS app's modal sheets. |
cdp-command | → device | { id, method, params?, sessionId? } | A remote Chrome DevTools Protocol command for a bmc connector. |
cdp-result | ← device | { id, ok, result?, error? } | The correlated reply to a cdp-command (matched by id). |
live-activity-message | → device | { message } | Update the device's Live Activity lock-screen pill text in realtime. |
Most
device:<uuid>events are a private control plane between Busymate's own tools and a device. They're documented here for completeness; integrators typically use the REST writes or the MCP tools that publish them rather than broadcasting directly.
Subscribing to a workspace feed
const channel = supabase
.channel("ws:" + WORKSPACE_ID, { config: { private: true } })
.on("broadcast", { event: "INSERT" }, ({ payload }) => {
// a newly captured entry
console.log("entry →", payload.record?.host);
})
.subscribe();The exact event names and payload shape on ws:<workspace_id> mirror the broadcast-from-database envelope ({ record, old_record, … }). Treat it as a live tail of new captures + breakpoint changes for that workspace.
postgres_changes (table-level CDC)
For tables you want to mirror, subscribe to row changes directly. The same capability gate applies — a token without the relevant view capability silently receives no rows.
const channel = supabase
.channel("device-watch")
.on(
"postgres_changes",
{ event: "*", schema: "public", table: "devices" },
({ eventType, new: row }) => console.log(eventType, row),
)
.subscribe();Tables published for CDC include devices, entries, breakpoint_events, workspace_tabs, tags, service_groups, device_status, settings_global, settings_device, and the single-row service_stats / service_status snapshot tables (see Stats & status for the live-monitor pattern).
devicesis published with full row identity, soUPDATEevents include the previous values — handy for detecting renames.
Access control
Channel subscription is RLS-gated server-side:
- You may join shared channels your role can read.
device:<uuid>and device-scoped fan-outs are limited to devices you own (or any device, if you're admin).- A token lacking the relevant capability never receives the events — it just stays quiet.
This is the same role model as REST and MCP. See Roles & permissions.
Troubleshooting
| Symptom | Cause / fix |
|---|---|
Channel subscribe() succeeds but no events arrive | Capability/ownership scope — your role can't read that channel, or the device isn't yours. Confirm the equivalent REST read works first. |
device:<uuid> join is rejected | You don't own that device and aren't admin. |
| Socket connects then drops | Verify the OAuth token is attached as Authorization: Bearer on the client and hasn't expired. |