Skip to content

Repository files navigation

saarthi

A self-hosted, plugin-extensible homelab stats dashboard. Single static Go binary, minimal Docker image, per-user customizable dashboards, and a public read-only share link for anything you want to show off without giving out access to the rest of your homelab.

Go License

Features

  • Built-in host stats — CPU, memory, network throughput, always on.
  • Plugin system — every other widget is a plugin: some ship built in, and users can add their own configured instances at runtime (no rebuild needed) from the dashboard's "Add widget" button.
  • Per-user dashboards — each authenticated user has their own saved widget layout and their own plugin instances (their own Traefik URL, their own weather location, ...).
  • Public share links — every user gets a /share/<token> URL they can turn on, exposing only the specific widgets they've explicitly marked public, read-only, no login required for viewers.
  • Centralized API key vault — enter a service's API key once on the Settings page, then pick it from a dropdown in any plugin instance that needs it, instead of re-typing it per instance.
  • Backup & restore — each user can download their own dashboard (instances, API keys, layout, share settings) as a JSON file and restore it later, live, no restart required.
  • Built-in login, or bring your own SSO — a first-run setup wizard creates the admin account; after that, anyone can sign up unless you disable it. If you set DASHBOARD_TRUST_AUTH_HEADER=true (only do this behind a reverse proxy that's guaranteed to strip/overwrite the header itself), a forward-auth header (Authentik, Authelia, Traefik forward-auth, ...) is trusted instead — so you can point this at your existing SSO and skip local accounts entirely.

Available plugins

Plugin What it shows Needs
CPU / Memory / Network Host utilization nothing (built in)
Traefik Router/service counts, request volume by status code Traefik API reachable, optionally its /metrics endpoint enabled
Immich Photo/video counts, library size, disk usage Immich API key
Nextcloud Users, files, active sessions, memory/CPU load Admin username + app password, serverinfo app enabled
Weather Current conditions for a chosen location nothing — uses Open-Meteo, no key
World Clock Live clocks for any timezones you list nothing
Stock Watchlist Quote + mini price history per symbol Finnhub API key (free tier)
Crypto Tracker Quote + 7-day sparkline per coin nothing — uses CoinGecko, no key

Quick start

curl -O https://raw.githubusercontent.com/rangoDJ/saarthi/main/docker-compose.yml
docker compose up -d

This pulls the published image from GHCR — nothing to build locally. Then:

  1. Open the dashboard — you'll land on a setup wizard to create the admin account (only shown while no accounts exist).
  2. Log in, click Settings, add any API keys you need under API keys, then add plugin instances from the main dashboard's + Add widget button.

Authentication

Two ways to authenticate, checked in this order:

  1. Forward-auth header — opt-in via DASHBOARD_TRUST_AUTH_HEADER=true. Only enable this if your reverse proxy is guaranteed to strip/overwrite the header itself before forwarding — otherwise anyone who can reach the dashboard port directly could set that header themselves and be authenticated as any user. Once enabled, if your reverse proxy (Authentik, Authelia, Traefik forward-auth, ...) sets an identity header on the request, saarthi trusts it outright and skips its own login entirely. Default header name is X-Authentik-Username; point DASHBOARD_AUTH_HEADER at whatever your proxy calls it (e.g. Authelia's Remote-User) — it accepts a comma-separated list, checked in order, if you need to accept more than one proxy's header name at once.
    • DASHBOARD_AUTH_HEADER_TRUSTED_PROXIES: optional comma-separated list of IPs/CIDRs. If set, the header is only trusted from a matching remote address, on top of the on/off switch above.
    • DASHBOARD_AUTH_HEADER_EXCLUSIVE=true: fully retires the local-login fallback below — identify() only ever trusts the header, so a stray local session can never authenticate a request once your SSO is confirmed working.
    • DASHBOARD_AUTH_NAME_HEADER / DASHBOARD_AUTH_EMAIL_HEADER: optional passthrough headers (e.g. Authentik's X-Authentik-Name / X-Authentik-Email) shown in the UI — display only, never used for access control.
    • DASHBOARD_AUTH_GROUPS_HEADER + DASHBOARD_AUTH_ADMIN_GROUP: if both are set and the proxy's groups header (comma-separated) contains the named group, the request is flagged admin in /api/me — a foundation for future admin-only routes; nothing is gated on it yet.
    • DASHBOARD_AUTHENTIK_LOGOUT_URL: optional. /logout clears saarthi's own cookie regardless, but that's a no-op for an SSO session — set this to also redirect the browser to your proxy's logout endpoint.
    • DASHBOARD_AUTHENTIK_OUTPOST_HEALTH_URL: optional. Settings shows whether this URL (e.g. your outpost's .../outpost.goauthentik.io/ping) is reachable, independent of whether any request actually carried the identity header.
  2. Built-in accounts — if no header is present (or forward-auth isn't enabled), saarthi falls back to its own login (bcrypt-hashed passwords, session cookies, a lockout after repeated failed attempts). The first account ever created (via the /setup wizard) is the admin; anyone else can self-register at /signup unless you set DASHBOARD_ALLOW_SIGNUPS=false. There's no admin/user permission split yet beyond "who created the account first" and the forward-auth group flag above — see Known limitations.

Configuration

All configuration is environment variables, prefixed DASHBOARD_.

Variable Default Purpose
DASHBOARD_HOST 0.0.0.0 Listen address
DASHBOARD_PORT 8090 Listen port
DASHBOARD_TOKEN (empty) Shared secret required (as X-Dashboard-Token) on mutating requests
DASHBOARD_ALLOWED_HOSTS (empty) Comma-separated allowlist of Host headers / CIDR ranges (DNS-rebinding protection)
DASHBOARD_TRUST_AUTH_HEADER false Set to true to trust the forward-auth header below — only do this behind a proxy that strips/overwrites it itself
DASHBOARD_AUTH_HEADER X-Authentik-Username Header(s) your reverse proxy sets with the authenticated username — comma-separated to accept more than one (ignored unless DASHBOARD_TRUST_AUTH_HEADER=true)
DASHBOARD_AUTH_HEADER_TRUSTED_PROXIES (empty) Comma-separated IPs/CIDRs the header is trusted from; empty means any remote
DASHBOARD_AUTH_HEADER_EXCLUSIVE false Set to true to disable the local-login fallback entirely once forward-auth is trusted
DASHBOARD_AUTH_NAME_HEADER / DASHBOARD_AUTH_EMAIL_HEADER (empty) Optional display-only passthrough headers (e.g. X-Authentik-Name)
DASHBOARD_AUTH_GROUPS_HEADER / DASHBOARD_AUTH_ADMIN_GROUP (empty) Optional: flags a request admin in /api/me if the named group is in the proxy's groups header
DASHBOARD_AUTHENTIK_LOGOUT_URL (empty) Optional: where /logout also redirects to end the SSO session
DASHBOARD_AUTHENTIK_OUTPOST_HEALTH_URL (empty) Optional: URL Settings pings to show forward-auth proxy reachability
DASHBOARD_ALLOW_SIGNUPS true Set to false to disable /signup (the setup wizard for the first admin account still always works)
DASHBOARD_ACCOUNTS_FILE <settings dir>/accounts.json Where local login accounts (bcrypt hashes only) are stored
DASHBOARD_SETTINGS_DIR data Directory for settings storage — global.json plus one users/<name>.json per user, created/read lazily as each user is seen
DASHBOARD_LOG_LEVEL INFO Set to DEBUG for verbose per-request and per-collector logging

Security note: if the server is reachable on all interfaces (DASHBOARD_HOST=0.0.0.0, the default) with neither DASHBOARD_TOKEN nor DASHBOARD_ALLOWED_HOSTS set, all mutating routes (saving settings, creating plugin instances, etc.) are automatically disabled at startup — a deliberate fail-safe rather than silently exposing them. Set one of the two before exposing this beyond localhost.

Architecture

cmd/dashboard/        entrypoint — loads config, blank-imports every plugin package, starts the server
internal/plugin/      the Widget interface + registry every plugin implements/self-registers into
internal/manager/     resolves each widget's effective active state (enabled + detected/reachable) and drives the scheduler
internal/scheduler/   runs each active widget's Collect() on its own interval, holds the latest-value snapshot
internal/settings/    JSON-file-backed persistence: global toggles, per-user instances/credentials/layout
internal/localauth/   bcrypt-hashed local account store (setup wizard, signups)
internal/session/     in-memory session cookies for local accounts
internal/auth/        request-context carrier for the resolved username (see internal/api/authflow.go for how it's resolved)
internal/api/         HTTP routes + security middleware
internal/plugins/*/   one package per plugin (cpu, mem, net, traefik, weather, worldclock, immich, nextcloud, stocks, crypto)
web/                  frontend — vanilla JS widget registry + GridStack.js grid, no build step

Writing a plugin

Every plugin implements plugin.Widget (ID(), Name(), Category(), Detect()) plus whichever of these it needs:

  • plugin.Collector — has a live value polled on an interval (Interval(), Collect()).
  • plugin.Configurable — needs user-supplied settings (SettingsFields(), ApplySettings()); this is what makes a plugin appear as an instantiable type on the "Add widget" flow.
  • plugin.RouteProvider — exposes its own mutating HTTP routes.
  • plugin.Redetectable — its availability can change while running (e.g. a socket that comes and goes), so it should be re-checked periodically.

Look at internal/plugins/weather/weather.go for the simplest complete example (no auth, one external API call), or internal/plugins/traefik/ for one with a settings form and best-effort partial data. Register it with plugin.RegisterFactory(typeID, name, func() plugin.Widget { return &Widget{} }) in an init(), then add a blank import in cmd/dashboard/main.go.

On the frontend, add a matching web/static/widgets/<name>.js file calling registerWidgetType({ type, template, onMetrics, ... }), and include the <script> tag in web/templates/index.html and share.html.

Development

go build ./...
go vet ./...
go run ./cmd/dashboard

Requires Go 1.25+. No cgo, no external tools — the built-in plugins (CPU/mem/net) and every optional plugin talk to plain HTTP APIs, so the binary is fully static and the runtime image needs nothing beyond ca-certificates.

Known limitations

  • No historical charts / SQLite-backed metrics history yet — every widget shows current/live values only (some, like stocks and crypto, include a short embedded sparkline from their own API).
  • No disk-usage plugin yet.
  • Third-party plugins currently require writing Go and rebuilding the image — there's no external/sidecar plugin protocol (yet).
  • No admin/user roles — every authenticated user can toggle the shared host-wide widgets (CPU/mem/net) on or off for everyone, and any account can back up/restore only its own data, but there's no privileged "manage other users" surface yet.
  • Sessions for local accounts are in-memory only — a server restart logs everyone out (forward-auth-header users are unaffected, since that identity comes from the proxy on every request).

License

MIT

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages