Stats & status API

Pull live fleet stats and infrastructure health over REST, MCP, or Realtime.

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

StatsStatus
REST RPCPOST /rest/v1/rpc/get_stats
REST tableGET /rest/v1/service_stats?id=eq.1&select=snapshotGET /rest/v1/service_status?id=eq.1&select=snapshot
MCP toolget_stats (no params)get_status (no params)
Realtimepostgres_changes UPDATE on public.service_statspostgres_changes UPDATE on public.service_status
Fed bya 60s cron recomputes the snapshotthe VPS status service every ~10s
Accesscapability stats:viewcapability 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:view capability.
  • Reading status requires the status:view capability.

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)

jsonc
{
  "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)

jsonc
{
  "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):

sh
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):

sh
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):

sh
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_stats recomputes on every call (freshest, but heavier). The service_stats table holds the last cron-computed snapshot — read it when "up to 60s old" is fine, or subscribe to it for pushes. There is no rpc/get_status; status is always read from the service_status snapshot table (or the MCP get_status tool, which reads the same row).

MCP (mcp.busymate.net)

Two parameter-less tools on the busymate-net MCP server:

ToolReturns
get_statsThe stats snapshot (same shape as the RPC).
get_statusThe status snapshot (host + component probes).
json
{"jsonrpc":"2.0","id":1,"method":"tools/call",
 "params":{"name":"get_stats","arguments":{}}}
json
{"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.

js
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

VPS status serviceevery ~10sstats cronevery 60sservice_statusid=1 · snapshotservice_statsid=1 · snapshotreadersREST · MCPRealtime WS

Troubleshooting

SymptomCause / 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 messageSame capability gate as REST — the OAuth token's user role lacks the capability.
Realtime never delivers an eventThe 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 oldExpected — it's cron-refreshed every 60s. Call rpc/get_stats for an on-demand fresh compute.