Heimdall: a Go terminal dashboard for hardware monitoring
Heimdall is a lightweight hardware monitoring system written in Go. It runs collectors on your machines, streams metrics over gRPC to a hub, and renders the fleet in a terminal dashboard built with Bubble Tea.
That alone would make it a useful tool for a homelab or a small fleet. What makes the repository worth reading is the way it keeps the moving parts separated: one binary per job, a narrow adapter contract for metrics, a versioned protobuf boundary, and an optional helper for privileged signals such as some GPU, power, and thermal data.
Five binaries, clear jobs
Heimdall’s executable entry points live under app/cmd/:
app/cmd/hub/main.goreceives metrics and fans them out to dashboards.app/cmd/dashboard/main.gorenders the terminal UI.app/cmd/daemon/main.gocollects host metrics and streams them to a hub.app/cmd/helper/main.goserves privileged metrics to the daemon when needed.app/cmd/cli/main.goprovides a JSON-friendly command-line client for scripts and agents.
That shape matches the architecture described in the README: daemon on every monitored host, hub on the monitoring station, dashboard wherever you want to view the fleet, and helper only on hosts that need privileged collection.
I like this because it avoids making one binary pretend to be everything. The dashboard collects nothing. The hub is not a terminal UI. The daemon does not need inbound ports for the normal data path. The helper is optional instead of being quietly folded into the unprivileged collector.
Go makes this style feel natural. Each command gets a small main.go, shared code lives under app/internal/, and internal gives the project an enforced boundary: other modules cannot import that application code accidentally.
The adapter contract is deliberately small
The central abstraction is app/internal/domain/adapter.go. At the time of writing, the interface is just:
type Adapter interface {
Describe() AdapterInfo
Collect(ctx context.Context) ([]Metric, error)
}
That is enough. Describe tells the system what the adapter can produce and whether it needs privilege. Collect does the work and returns domain metrics. The concrete implementations live in app/internal/adapters/, with separate files for CPU, memory, swap, load, frequency, disk, disk I/O, temperature, network, reachability, uptime, gateway, inventory, and the helper bridge.
The practical effect is that adding a signal is boring in the best way: write a new adapter, register it in adapters.Build, and give it tests. You do not have to thread a new metric through a giant collector function.
The project documentation calls this out in ADR 0003, which focuses on metric adapter contracts and failure isolation. A monitoring system has to expect partial failure. A temperature probe might be missing, a power metric might require privilege, a vendor command might fail, and a network check might time out. Those cases should degrade that metric, not take the whole host offline.
Cross-platform does not mean hiding every detail
Heimdall uses gopsutil for a lot of host data, which is a sensible choice. CPU, memory, disk, host, process, and network collection already have awkward platform differences, and gopsutil gives Go projects a maintained abstraction over many of them.
But Heimdall does not stop at “wrap gopsutil and call it done.” Its adapter layer still gives the project its own contract, status model, and failure behavior. The metric that reaches the dashboard is a Heimdall domain metric, not a leaked third-party type.
That distinction is useful. Dependencies can collect raw facts, while your application still decides what “unavailable”, “insufficient permission”, or “error” means to users. In a terminal dashboard, that nuance matters more than a naked zero value.
gRPC is the wire boundary
The README describes Heimdall’s normal data path as a low-bandwidth gRPC link from unprivileged daemons to the hub. The source matches that: app/internal/transport/ maps domain metrics to generated protobuf types under common/proto/monitoring/v1.
That is a good boundary to copy. The domain package does not need to import generated protobuf structs. Mapping happens at the edge, where transport concerns belong. The hub, daemon, dashboard, and CLI can share a versioned contract without forcing the rest of the codebase to speak wire DTOs internally.
The architecture also gives Heimdall room to grow. The hub can support multiple dashboards. The daemon can reconnect. The CLI can query the hub in JSON for scripts or agents. OpenMetrics export can sit behind the hub for Prometheus or Grafana without turning the dashboard into the source of truth.
Privilege is isolated
The helper binary is the part the original draft of this post missed. That omission matters.
Heimdall is unprivileged by default, but some metrics are inherently platform- or permission-sensitive. On Linux, power and GPU paths may involve RAPL, hwmon, nvidia-smi, AMD tooling, or sysfs. On macOS, Apple Silicon power and GPU information can involve IOReport or SMC paths. On Windows, thermal and GPU data have their own APIs and limitations.
Rather than requiring the whole daemon to run as root or admin, Heimdall uses an optional heimdall-helper for privileged metrics and commands. The unprivileged adapters can report that a metric needs the helper instead of failing silently or escalating everything.
That is the right security posture for a monitoring tool. Most metrics should work without special privilege. The exceptional cases should be explicit, isolated, and documented.
The TUI is a presentation layer
The terminal UI lives under app/internal/tui/, with separate packages for dashboard views, top view, rendering, theme, splash, and brand elements. Heimdall depends on Bubble Tea and Lip Gloss, which is a common modern stack for Go terminal interfaces.
The important architectural point is that the dashboard is presentation, not collection. It subscribes to the hub, renders host state, shows details, and exposes interactions such as logs, process views, and commands when the daemon has opted into those features.
That separation keeps the UI honest. A dashboard bug should not change how metrics are sampled. A new rendering mode should not require rewriting the daemon. A headless CLI can still talk to the same hub because the useful state is not trapped inside terminal widgets.
Alerting and export stay near the hub
Heimdall also has packages for alerting, OpenMetrics export, history, and storage:
app/internal/alert/contains the rule engine and notifier types.app/internal/observe/handles history and OpenMetrics serving.app/internal/store/contains Prometheus-related storage code.
Those packages fit the overall design. The hub is the place that sees the fleet, so it is the natural place to evaluate fleet-level rules and expose scrapeable metrics. The dashboard can show alert state, but it does not have to own alert evaluation.
That is another pattern worth borrowing. If you have a collector, aggregator, and UI, resist the urge to put operational logic in the UI just because that is where a human sees it first.
What Go developers can take from Heimdall
Use separate binaries when the jobs are genuinely different. daemon, hub, dashboard, helper, and cli are easier to understand than one command with a pile of modes and hidden privilege assumptions.
Keep metric collection behind a project-owned interface. Third-party libraries can handle platform details, but your application still needs its own status model, tests, and failure behavior.
Treat generated protobuf types as transport details. Map at the edge and keep domain code free of wire-format dependencies.
Make privileged paths explicit. A helper process with a small job is easier to reason about than an always-privileged daemon.
Keep the terminal UI as a subscriber, not a collector. That gives you room for dashboards, CLIs, exports, and automation to share the same source of truth.
Heimdall is still a young project, but its boundaries are already clear. If you are building monitoring, diagnostics, or infrastructure tooling in Go, it is a useful repository to read before you start drawing your own daemon-hub-dashboard architecture.