Roomy / Documentation
v1.3.0 · self-hosted

Roomy Hub reference

The central view of a fleet. Every host running roomy fleet posts a short report after each run; the hub keeps an inventory, a history per host, fleet-wide totals and metrics, serves a dashboard, and hands out centrally managed policies. One binary, plain files, no database.

Last reviewed: October 27, 2026 · Product overview and screenshots: Roomy Fleet & Hub

Overview

roomy hub serve --listen 0.0.0.0:9478 --data-dir /var/lib/roomy-hub \
                --agent-token-file /etc/roomy/hub.agent.token --admin-token-file /etc/roomy/hub.admin.token
roomy hub status | hosts | host NAME | remove NAME | events      # talk to a hub (--url, --token)
roomy hub policy list | get NAME | push NAME FILE | remove NAME  # centrally managed policies
roomy hub demo --data-dir ~/.roomy/hub-demo --hosts 12            # synthetic fleet to try the dashboard
roomy hub token                                                   # random token for the two files above

The hub is built like the monitoring you already run: hosts push a few kilobytes of plain JSON over HTTP on a timer, the hub writes them to disk, and the dashboard and metrics are computed from those files. There is no agent to install beyond roomy itself, no cloud component and no telemetry.

Roomy Hub overview: hosts by status, reclaimable by tier, freed in the last 24 hours, freed per day, reclaimable by category, hosts with the most to reclaim, largest findings

The screenshot is the built-in demo fleet (roomy hub demo), not a customer.

Running the hub

On a laptop, to have a look

roomy hub demo && roomy hub serve --data-dir ~/.roomy/hub-demo
open http://127.0.0.1:9478/

roomy hub serve with no flags listens on 127.0.0.1:9478 with no tokens, which is fine on localhost.

With systemd (from the packages)

roomy hub token | sudo tee /etc/roomy/hub.agent.token >/dev/null
roomy hub token | sudo tee /etc/roomy/hub.admin.token >/dev/null
sudo chmod 600 /etc/roomy/hub.*.token
sudoedit /etc/roomy/hub.env            # ROOMY_HUB_LISTEN=0.0.0.0:9478, ROOMY_HUB_TITLE=…
sudo systemctl enable --now roomy-hub.service

roomy-hub.service runs as a DynamicUser with state in /var/lib/roomy-hub, settings in /etc/roomy/hub.env, and the two token files above.

In a container

docker run -d --name roomy-hub -p 9478:9478 -v roomy-hub:/data ghcr.io/tdermendjiev/roomy \
  hub serve --listen 0.0.0.0:9478 --data-dir /data --agent-token … --admin-token …

Options

FlagDefaultMeaning
--listen H:P127.0.0.1:9478Address to bind. Also $ROOMY_HUB_LISTEN in the service.
--data-dir DIR~/.roomy/hubWhere hosts, policies and events are kept. Also $ROOMY_HUB_DATA.
--agent-token T / --agent-token-file FnoneToken hosts use to post reports and fetch policies.
--admin-token T / --admin-token-file FnoneToken for the dashboard, the API and metrics.
--stale-after 24h24hWhen a host with unknown interval counts as stale.
--license TOKENsee EditionsFleet license; sets the host allowance and the organisation name.
--title NAMERoomy HubShown in the dashboard header.

Tokens and security

Two bearer tokens, compared in constant time.

  • The agent token can only post reports and fetch managed policies. A compromised host cannot read the inventory or change a policy.
  • The admin token does everything else: the dashboard, the API, metrics, pushing policies, removing hosts.

Pass a token as Authorization: Bearer <token> or as ?token=. Without any token configured the API is open, which is fine on 127.0.0.1 and a warning anywhere else. The dashboard asks for the admin token once and keeps it in the browser's local storage.

The hub speaks plain HTTP and has no TLS of its own. Put it on a private network or behind your reverse proxy with TLS, like node_exporter or Prometheus itself. Hosts and the roomy hub CLI handle an https:// hub URL through the system curl, so TLS to a reverse proxy works without Roomy linking a TLS stack.

How a host reports

In the host's policy:

{ "hub": { "url": "http://hub.internal:9478", "tokenFile": "/etc/roomy/hub.token",
           "tags": { "role": "agent", "team": "platform" }, "policy": "agent-host" } }

or, when writing the policy:

roomy fleet policy init --profile agent-host --hub http://hub.internal:9478 \
  --hub-policy agent-host --tag role=agent --output /etc/roomy/policy.json

After every roomy fleet run (and every cycle of fleet serve) the host posts a HubHostReport: host, platform, Roomy and catalog version, policy name, edition, tags, what the scan found by tier and category, what the policy selected, what was removed, cumulative totals from the audit log, quarantine size, disk space, the audit chain head (seq + hash), and the largest findings (hub.topItems, default 25, warning tier included so the dashboard can show what the policy is not allowed to remove). A few kilobytes. Never file contents.

The audit log records hubReported, or error with the reason if the hub was unreachable. A hub outage never stops a run, and never makes one more aggressive.

fleet serve reports its interval so the hub can tell a quiet host from a dead one; timer-driven hosts set hub.intervalSeconds or inherit the hub's --stale-after.

Centrally managed policies

roomy hub policy push ci-runner policy.json     # validate and store on the hub
roomy hub policy list
roomy hub policy get ci-runner

A host whose local policy sets hub.policy: "ci-runner" fetches it before every run and uses it instead of the local file, keeping only the local hub, license, scan.home and lockFile sections. If the hub cannot be reached the host runs its local policy and says so in the log. --local-policy on fleet run and fleet serve skips the pull.

So the local file on each host can be a two-line bootstrap, and a policy change is one push plus the next run. The Policies view shows each managed policy and which hosts run it.

Roomy Hub policies view: managed policies with their JSON and the hosts using each policy name

Host status

StatusMeaning
healthyReported within the window, last run had no failures, disk above 10% free.
attentionReported, but: disposal failures in the last run, under 10% free, over the license allowance, or an expired fleet license.
staleNo report for 2.5× the host's interval (minimum 15 minutes), or --stale-after when the interval is unknown.

Hosts that are over the license allowance still appear with their reports; only new hosts beyond it are refused with HTTP 402 and an event. Removing a host (roomy hub remove, or the dashboard) drops it from the inventory; it comes back on its next report.

Dashboard

GET / is a single self-contained page with no external assets, so it works air-gapped. It refreshes every 30 seconds.

  • Overview: hosts by status, reclaimable by tier, freed in the last 24 h / 7 d / all-time, hosts under disk pressure, quarantine held; freed per day for 14 days; reclaimable by category; the hosts with the most to reclaim, by tier; the largest findings across the fleet; hosts that need attention; recent runs.
  • Hosts: searchable, filterable, sortable inventory with tags, status reasons, tier bars, disk meter, a reclaimable sparkline and last-report age.
  • Host: everything in the last report, reclaimable over time, freed per run, the largest findings with tier and idle time, why items were skipped, audit chain head, an operator note, and removal.
  • Policies: hub-managed policies and which hosts use each policy name.
  • Activity: the hub's own event log (reports, rejections, policy pushes, removals).
Roomy Hub host detail: last report, reclaimable over time, freed per run, largest findings with tier and idle time, skipped reasons, audit chain head

API

All JSON. Agent routes accept the agent or admin token; everything else needs the admin token.

RouteWhoWhat
POST /api/v1/reportagentIngest a HubHostReport; 402 when a new host exceeds the allowance
GET /api/v1/policies, GET /api/v1/policies/{name}agentList / fetch a managed policy
PUT /api/v1/policies/{name} (body: policy JSON), DELETE …adminStore (validated) / remove
GET /api/v1/summaryadminThe HubSummary the overview is drawn from
GET /api/v1/hostsadminInventory rows with status (history trimmed, top items omitted)
GET /api/v1/hosts/{name}, DELETE …, PUT …/note (text body)adminOne host in full / remove / note
GET /api/v1/events?n=50adminRecent hub events
GET /api/v1/licenseadminEdition, org, allowance, hosts over it
GET /api/v1/infopublicName, version, whether auth is required
GET /metricsadminFleet-wide Prometheus text
GET /healthzpublicok

Shapes (HubHostReport, HubSummary, HubHostRecord, HubEvent) are versioned by schemaVersion and additive-only, like ScanReport.

curl -s -H "Authorization: Bearer $ADMIN" http://hub.internal:9478/api/v1/hosts | jq '.[] | {host, status, reclaimableBytes}'

Metrics

Scrape /metrics with the admin token (authorization: credentials in the Prometheus job). One scrape covers the whole fleet.

Fleet series

roomy_hub_hosts{status}, roomy_hub_license_hosts_max, roomy_hub_reclaimable_bytes{tier}, roomy_hub_reclaimable_bytes_by_category{category}, roomy_hub_disposed_bytes_total, roomy_hub_disposed_bytes_last_24h, roomy_hub_dispose_failures_total, roomy_hub_quarantine_bytes, roomy_hub_volume_available_bytes, roomy_hub_volume_total_bytes, roomy_hub_hosts_low_disk.

Per-host series

Each carries host: roomy_hub_host_info{platform,version,policy,edition,status}, roomy_hub_host_reclaimable_bytes{tier}, roomy_hub_host_disposed_bytes_total, roomy_hub_host_last_run_disposed_bytes, roomy_hub_host_last_run_failures, roomy_hub_host_last_run_success, roomy_hub_host_dry_run, roomy_hub_host_last_report_timestamp_seconds, roomy_hub_host_quarantine_bytes, roomy_hub_host_volume_available_bytes, roomy_hub_host_volume_total_bytes, roomy_hub_host_audit_seq.

Alerts worth having

roomy_hub_hosts{status="stale"} > 0                                              # a host stopped reporting
roomy_hub_host_volume_available_bytes / roomy_hub_host_volume_total_bytes < 0.1   # disk pressure despite cleanup
increase(roomy_hub_dispose_failures_total[1d]) > 0                               # something could not be removed
delta(roomy_hub_host_audit_seq[1h]) < 0                                           # a host's audit log was reset

Data directory

/var/lib/roomy-hub/
  hosts/<host>.json      latest report + up to 1000 history points + note
  policies/<name>.json   managed policies, exactly as pushed
  events.jsonl           hub event log

Plain files: back up with rsync, inspect with jq, delete a host file to forget it. The hub reloads everything on start. Request bodies are capped at 8 MB.

Editions

Without a license the hub tracks up to 5 hosts, enough to try it with real machines. A fleet license sets the allowance to its maxHosts and shows the organisation in the header. The hub reads its license from --license, $ROOMY_LICENSE, /etc/roomy/license.key or ~/.roomy/license.key, and verifies it offline.

When the allowance is reached, a report from a host the hub has not seen is refused with HTTP 402 and logged as an event; known hosts keep reporting. An expired license therefore degrades to an "attention" marker, not an outage. Licensing details are in the Fleet reference; pricing is on the Fleet page.

CLI commands

All operator commands take --url and --token, or read $ROOMY_HUB_URL, $ROOMY_HUB_TOKEN and ~/.roomy/hub-admin.token. Add --json for machine output.

roomy hub status                       # fleet totals, hosts by status, license
roomy hub hosts [--status attention]   # inventory table
roomy hub host agent-eu-03 [--note "Decommission next week"]
roomy hub remove agent-eu-03
roomy hub events -n 50
roomy hub policy list | get NAME | push NAME FILE | remove NAME
roomy hub demo [--hosts 24] [--seed 7] [--org "Example"] [--reset]   # synthetic fleet in ~/.roomy/hub-demo
roomy hub token                        # print a random token

Home · Mac app · CLI · Fleet · Fleet & Hub overview · Privacy · EULA · Source on GitHub