Pull live fleet stats and infrastructure health over REST, MCP, or Realtime — two single-row snapshot feeds, three ways to read them, all behind the same OAuth token.
Two live, read-only feeds describe the running system:
- Service stats — what the fleet is doing: device counts, entry totals, request throughput, status/source/connection-type mixes, and the busiest domains + devices.
- Service status — whether the infra is healthy: host CPU / load / memory / uptime plus a per-component health probe (HTTP reachability, latency, systemd unit state).
Each is available over all three programmatic surfaces — REST (api.busymate.net), MCP (mcp.busymate.net), and Realtime WS (Supabase) — so you can poll once, call a tool, or subscribe for live pushes.
TL;DR
| Stats | Status | |
|---|---|---|
| REST RPC | POST /rest/v1/rpc/get_stats | — |
| REST table | GET /rest/v1/service_stats?id=eq.1&select=snapshot | GET /rest/v1/service_status?id=eq.1&select=snapshot |
| MCP tool | get_stats (no params) | get_status (no params) |
| Realtime | postgres_changes UPDATE on public.service_stats | postgres_changes UPDATE on public.service_status |
| Fed by | a 60s cron recomputes the snapshot | the VPS status service every ~10s |
| Access | capability stats:view | capability status:view |
Both snapshot tables are single-row (id = 1); the freshest value always lives at that row, and a write replaces the snapshot jsonb in place — so a Realtime UPDATE event is the live push.
Access — capability-gated
These surfaces are RBAC-gated, not public:
- Reading stats requires the
stats:viewcapability. - Reading status requires the
status:viewcapability.
The built-in admin role grants both (it grants every capability); custom roles get them via Settings → Roles. RLS enforces this server-side through has_capability(section, action), so a token without the capability gets an empty result over REST/Realtime and an error over MCP — see Roles & permissions. Every surface authenticates with an OAuth access token (the same token type MCP, PostgREST, and Realtime all accept).
Shapes
Stats snapshot (get_stats / service_stats.snapshot)
{
"total_devices": 12,
"online_devices": 4,
"total_entries": 184203,
"entries_1h": 512,
"entries_24h": 9841,
"requests_per_sec": 0.14,
"status_class_mix": { "2": 7321, "3": 88, "4": 410, "5": 12 },
"source_mix": { "vpn": 6200, "pac": 1900, "cdp": 1741 },
"connection_type_mix": { "vpn": 9, "pac": 3 },
"top_domains": [ { "host": "api.stripe.com", "count": 1840 } ],
"top_devices": [ { "device_uuid": "…", "name": "BMH3", "count": 5120 } ],
"generated_at": "2026-06-02T10:00:00.000Z"
}Status snapshot (get_status / service_status.snapshot)
{
"overall": "ok", // ok | degraded | down
"host": {
"cpu": 0.07, // load fraction 0..1
"load": [0.21, 0.18, 0.15], // 1/5/15-min load averages
"mem": { "usedMb": 1840, "totalMb": 3920 },
"uptime": 1820394 // seconds
},
"components": [
{
"key": "dashboard",
"label": "Dashboard",
"probe": { "up": true, "httpStatus": 200, "latencyMs": 42 },
"unitState": "active" // systemd ActiveState, where applicable
}
]
}overall rolls up the component probes; a component is unhealthy when its probe.up is false (or its unitState isn't active).
REST (api.busymate.net)
api.busymate.net is a CNAME to the Supabase project, so these are plain PostgREST calls. Pass your token as Authorization: Bearer <token> and apikey.
Stats via the RPC (computes a fresh snapshot on call):
curl -s -X POST https://api.busymate.net/rest/v1/rpc/get_stats \
-H "Authorization: Bearer $TOKEN" \
-H "apikey: $TOKEN" \
-H "content-type: application/json" \
-d '{}'Status — read the cached snapshot (cheap; updated every ~10s by the VPS status service):
curl -s "https://api.busymate.net/rest/v1/service_status?id=eq.1&select=snapshot" \
-H "Authorization: Bearer $TOKEN" \
-H "apikey: $TOKEN"Stats — read the cached snapshot (cheaper than the RPC; refreshed by the 60s cron):
curl -s "https://api.busymate.net/rest/v1/service_stats?id=eq.1&select=snapshot" \
-H "Authorization: Bearer $TOKEN" \
-H "apikey: $TOKEN"Both table reads return a one-element array; the payload is in [0].snapshot.
RPC vs. table.
rpc/get_statsrecomputes on every call (freshest, but heavier). Theservice_statstable holds the last cron-computed snapshot — read it when "up to 60s old" is fine, or subscribe to it for pushes. There is norpc/get_status; status is always read from theservice_statussnapshot table (or the MCPget_statustool, which reads the same row).
MCP (mcp.busymate.net)
Two parameter-less tools on the busymate-net MCP server:
| Tool | Returns |
|---|---|
get_stats | The stats snapshot (same shape as the RPC). |
get_status | The status snapshot (host + component probes). |
{"jsonrpc":"2.0","id":1,"method":"tools/call",
"params":{"name":"get_stats","arguments":{}}}{"jsonrpc":"2.0","id":2,"method":"tools/call",
"params":{"name":"get_status","arguments":{}}}Send either as the -d body of the MCP JSON-RPC request — see MCP → Connect. The result is a content[].text JSON string.
Realtime WS (live pushes)
Subscribe to postgres_changes UPDATE events on the two single-row tables to get the snapshot pushed the instant it changes — no polling. service_status updates every ~10s; service_stats every 60s.
import { createClient } from "@supabase/supabase-js";
const supabase = createClient("https://api.busymate.net", PUBLISHABLE_KEY, {
global: { headers: { Authorization: `Bearer ${TOKEN}` } },
});
const channel = supabase
.channel("service-monitors")
.on(
"postgres_changes",
{ event: "UPDATE", schema: "public", table: "service_status", filter: "id=eq.1" },
({ new: row }) => console.log("status →", row.snapshot.overall, row.snapshot.host),
)
.on(
"postgres_changes",
{ event: "UPDATE", schema: "public", table: "service_stats", filter: "id=eq.1" },
({ new: row }) => console.log("stats →", row.snapshot.requests_per_sec, "req/s"),
)
.subscribe();The new.snapshot on each event is the full jsonb above — read the current value once over REST on connect, then let Realtime keep it fresh. The same stats:view / status:view capability gates the Realtime subscription, so a token lacking it simply never receives rows.
Where the snapshots come from
Troubleshooting
| Symptom | Cause / fix |
|---|---|
REST read returns [] (empty array) | Your token lacks stats:view / status:view, or the snapshot row hasn't been written yet. Check your role in Roles & permissions. |
MCP get_stats / get_status errors with a permission message | Same capability gate as REST — the OAuth token's user role lacks the capability. |
| Realtime never delivers an event | The capability gate also applies to subscriptions; a token without it silently receives no rows. Confirm the read works over REST first. |
service_status looks stale (> ~30s old) | The VPS status service is down or wedged — the overall/host won't refresh. Check the dashboard status surface or the VPS unit. |
service_stats up to 60s old | Expected — it's cron-refreshed every 60s. Call rpc/get_stats for an on-demand fresh compute. |