Multi-machine aggregation
OpenUsage runs per-machine by default — the dashboard sees only what the local daemon collected. When you want a single pane that shows spend across multiple workstations (e.g. work laptop, home desktop, dedicated build host), the Hub + Exporter pair gives you that without changing how local data is collected.
Architecture
machine A (openusage telemetry)
└─ Exporter ──POST /v1/push──▶ Hub server ◀── openusage hub-view <url>
machine B (openusage telemetry)
└─ Exporter ──POST /v1/push──▶ (in-memory Store)
Every worker still runs the normal openusage telemetry daemon — collection logic, providers, and the SQLite store are unchanged. The new piece is an exporter inside the daemon that, when export.target is set, periodically POSTs the latest UsageSnapshot batch to a remote hub. The hub holds the latest snapshot per machine in memory and exposes:
| Endpoint | Method | Auth required |
|---|---|---|
/v1/push | POST | Bearer (if a token is configured) |
/v1/snapshots | GET | Bearer (if a token is configured) |
/healthz | GET | never (liveness probe) |
openusage hub provides a built-in TUI for the aggregated view. openusage hub-view <url> is a read-only client suitable for a laptop that doesn't need its own daemon.
Step 1: choose where the hub runs
Pick one host that all workers can reach. Common picks:
- A home-lab box or always-on workstation on the same LAN
- A small VPS, exposed via Tailscale / WireGuard / Cloudflare Tunnel
- A docker host running the hub container
Step 2: pick an auth posture
| Scenario | auth token | bind |
|---|---|---|
| Trusted home LAN, just for personal visibility | none | 127.0.0.1:9190 (loopback only) |
| Trusted LAN, accessed from another LAN machine | none, --allow-public | :9190 |
| Reachable over Tailscale / WireGuard / VPN | OPENUSAGE_HUB_TOKEN set | :9190 |
| Public internet | OPENUSAGE_HUB_TOKEN set | :9190 + TLS terminator in front |
Recommendation: always set a token if anything other than this machine can reach the port. Once set, every push or snapshot fetch must include Authorization: Bearer <token>.
:::warning Tokens are never written to settings.json
The token lives in your shell environment (OPENUSAGE_HUB_TOKEN), not in ~/.config/openusage/settings.json. This matches the accounts[].api_key_env convention: configs reference secrets by env-var name; secrets themselves never sit on disk.
:::
Step 3: start the hub
As a normal process
# Trusted LAN, no auth
openusage hub --listen :9190 --allow-public
# With Bearer auth, headless (for systemd / launchd / containers)
export OPENUSAGE_HUB_TOKEN=$(openssl rand -hex 32)
openusage hub --headless
The unsafe-default guard refuses to bind a non-loopback interface when no auth is configured. You'll see one of three remediations in the startup error:
hub: refusing to listen on ":9190" without auth_token.
Choose one:
1. export OPENUSAGE_HUB_TOKEN=<secret> to enable Bearer auth, OR
2. bind to loopback only: --listen 127.0.0.1:9190, OR
3. pass --allow-public if you have a network-level firewall in place
In Docker
A Dockerfile.hub is included at the repo root. Build it locally:
docker build -f Dockerfile.hub -t openusage-hub:dev .
docker run --rm \
-e OPENUSAGE_HUB_TOKEN=$(openssl rand -hex 32) \
-p 9190:9190 \
openusage-hub:dev
The container is for the hub server only — it does not run the TUI dashboard. Expect a published image and a versioned release tag to follow in a separate PR.
Key properties of the image:
- Non-root user (
USER 65534:65534/nobody) HEALTHCHECKagainst/healthzEXPOSE 9190- OCI labels (
source,version,revision,created,licenses)
Step 4: enable the exporter on each worker
On every machine you want feeding the hub, edit ~/.config/openusage/settings.json:
{
"export": {
"target": "http://hub.lan:9190",
"interval_seconds": 60,
"machine_name": "work-laptop"
}
}
If the hub requires auth, OPENUSAGE_HUB_TOKEN needs to live in the daemon's environment — not just your interactive shell. How you set it depends on how you run the daemon.
# Foreground daemon
OPENUSAGE_HUB_TOKEN=<secret> openusage telemetry daemon
For a user-managed service, put the token in the service environment:
- Linux/systemd user service — add
OPENUSAGE_HUB_TOKEN="<secret>"to the unit'sEnvironmentFile=or addEnvironment=OPENUSAGE_HUB_TOKEN=<secret>under[Service], then runsystemctl --user daemon-reload && systemctl --user restart openusage-telemetry. - macOS/launchd — add
OPENUSAGE_HUB_TOKENto the plist'sEnvironmentVariablesdictionary, then reload withlaunchctl unload ~/Library/LaunchAgents/com.openusage.telemetryd.plist && launchctl load ~/Library/LaunchAgents/com.openusage.telemetryd.plist.
The exporter pushes immediately on startup and then every interval_seconds. Best-effort: errors are logged and swallowed; the daemon never stops over an exporter failure.
Step 5: view the aggregated dashboard
From a machine with a daemon
openusage hub --listen 127.0.0.1:9190
Same TUI as the local dashboard. Each provider tile is keyed by machine:providerID:accountID so two machines running the same provider don't collide.
From a laptop with no daemon
openusage hub-view http://hub.lan:9190
OPENUSAGE_HUB_TOKEN=s3cret openusage hub-view https://openusage.example.com
hub-view polls GET /v1/snapshots on the ui.refresh_interval_seconds cadence (override with --interval). The status line shows hub <url> · N machine snapshots and flips to an error if the hub becomes unreachable.
Operational notes
- Stale eviction. A machine entry is pruned after
hub.stale_timeout_seconds(default 300s). Stop a worker and within 5 min its tiles disappear from the aggregated view. - Snapshot is the latest, not a stream. The hub holds only the newest batch per machine. If you want historical aggregates, query each daemon's SQLite separately.
/healthzis unauthenticated by design. Liveness probes work without secrets; the response only lists machine names — no snapshot data. On a trusted LAN this is fine, but on an internet-facing hub the machine list leaks topology. If you care, bind the hub loopback-only and reverse-proxy/v1/pushand/v1/snapshotsvia your existing TLS terminator, or firewall/healthzat the network layer.- Bind address matters.
127.0.0.1:9190is loopback-only and safe even without auth;:9190or0.0.0.0:9190is all-interfaces and requires either auth or--allow-public.
See also
openusage hubandopenusage hub-view— flag referenceexportandhubconfig blocks — settings.json fieldsOPENUSAGE_HUB_TOKEN— the shared Bearer-token env var- Headless servers — running the daemon on hosts without a desktop