Beszel is a lightweight, self-hosted server-monitoring hub. It watches CPU, memory, disk, temperature, load, and up/down status across your machines and raises an alert when a metric crosses a threshold. This score reads those alerts and renders them on Tempo’s timeline as structured events, one stack per host and metric, so a flapping monitor reads as a single entry instead of a flood.

This score is built into Tempo. It ships with the app and is seeded into ~/Library/Application Support/Tempo/Scores/ on first launch, so there is nothing to download for the score itself. The work below is setting up the companion helper once.

How it works

Beszel can already send notifications through Shoutrrr, but its generic:// webhook carries a title and a message string only. Beszel flattens an alert before Shoutrrr sees it, so the metric, the threshold, and the triggered-or-resolved state never reach Tempo. A title alone is not enough to assign severity or to group a flap cycle.

The helper takes a different path. Beszel runs on PocketBase, and PocketBase exposes the structured alerts_history records over its HTTP API. The helper is a small Python poller (beszel-tempo.py) that signs in as a read-only user, reads those records, and posts a full event to Tempo on each alert transition, with the system name, the metric, and the state intact.

Beszel hub (PocketBase on :8090)
      |  HTTP, read-only PocketBase API
beszel-tempo.py  (polls alerts_history, builds a Tempo event)
      |  HTTP, with the per-provider Tempo token
Tempo ingestion server  on <your-mac>:7776

The helper posts only on transitions (an alert triggered, then later resolved). It does not stream the raw telemetry, and it does not post on a heartbeat. The score handles what happens after the event lands: the severity, the grouping, and the action buttons.

Run the helper on a host that can reach the Beszel hub, typically the hub box itself. This guide does not use Beszel’s own notifications. If you had already pointed a Beszel generic:// notification at Tempo before setting up the helper, remove it, so the same alert does not arrive twice.

What you need

1. Create a read-only poller user in Beszel

Beszel is PocketBase, so the poller authenticates as a normal users record. Open the PocketBase admin UI at http://<hub>:8090/_/, sign in as superuser, and create a dedicated user for the poller, for example tempo-poller@your-domain.

The alerts_history and systems collections are owner-scoped and share-scoped, so a fresh user sees nothing by default. As superuser, edit the collection rules so the poller can read every record while staying read-only:

# alerts_history.listRule  (keeps each user's own access, lets the poller read all)
@request.auth.id != "" && (user = @request.auth.id || @request.auth.id = "<POLLER_USER_ID>")

# systems.listRule AND systems.viewRule  (needed to expand system IDs to names)
@request.auth.id != "" && (users.id ?= @request.auth.id || @request.auth.id = "<POLLER_USER_ID>")

Leave the create, update, and delete rules untouched. The poller never writes; a write attempt returns 403. Note the caveat in the troubleshooting section: a Beszel upgrade can reset these rules.

2. Create the Tempo token

In Tempo, open Settings → Ingestion and create a token bound to com.beszel. Copy its value; the helper needs it. Your ingestion endpoint is http://<your-mac-hostname>:7776/ingest.

3. Get the helper

In the Score Editor, select Beszel and open the Source tab. The Helper section has Open in Finder and Open README. Open in Finder copies the helper package to ~/Library/Application Support/Tempo/Integrations/com.beszel/ and reveals it. The package holds the poller script beszel-tempo.py and a copy of its README. Copy beszel-tempo.py to the host that will run it.

4. Configure the helper

Create an environment file next to the script, for example ~/tempo-beszel/beszel.env, readable only by you (chmod 600). Keep the two secrets, the Tempo token and the Beszel password, in their own files rather than inline:

export BESZEL_URL=http://127.0.0.1:8090
export BESZEL_USER=tempo-poller@your-domain
export TEMPO_URL=http://<your-mac-ip>:7776/ingest
export STATE_FILE=/home/<you>/tempo-beszel/beszel-state.json
export POLL_SECONDS=30

# Secrets, read from their own chmod 600 files, not stored in this env file:
export TEMPO_TOKEN_FILE=/home/<you>/tempo-beszel/tempo-token
export BESZEL_PASS_FILE=/home/<you>/tempo-beszel/beszel-pass

The poller resolves each secret from ${VAR}_FILE first, then from a plain ${VAR} environment variable. Pointing at a file keeps the com.beszel token and the Beszel password out of the env file: use any chmod 600 file, a Docker secret on tmpfs, or a systemd-creds credential. If you would rather keep it simple, you can set TEMPO_TOKEN and BESZEL_PASS inline in the env file instead, since it is already chmod 600. The Beszel password is used only against the hub and is never sent to Tempo.

On first run the poller seeds its state silently, so historical alerts already in alerts_history do not flood the timeline. Only new transitions after that produce events.

5. Run the helper as a daemon

The poller is a long-lived process. It should restart on crash and start on boot. A small run wrapper sources the env file and execs the script, logging to a file:

#!/bin/sh
# beszel-run.sh
. /home/<you>/tempo-beszel/beszel.env
exec python3 /home/<you>/tempo-beszel/beszel-tempo.py \
  >> /home/<you>/tempo-beszel/beszel-watcher.log 2>&1

Linux (flock keepalive cron)

The simplest persistence is a flock keepalive cron. It runs a single instance, restarts within five minutes if the process dies, and starts on boot:

*/5 * * * * flock -n /home/<you>/tempo-beszel/beszel.lock /home/<you>/tempo-beszel/beszel-run.sh
@reboot     flock -n /home/<you>/tempo-beszel/beszel.lock /home/<you>/tempo-beszel/beszel-run.sh

Linux (systemd)

If you prefer systemd, create /etc/systemd/system/tempo-beszel.service:

[Unit]
Description=Tempo Beszel poller
After=network-online.target

[Service]
ExecStart=/usr/bin/python3 /home/youruser/tempo-beszel/beszel-tempo.py
EnvironmentFile=/home/youruser/tempo-beszel/beszel.env
Restart=always

[Install]
WantedBy=multi-user.target

Then sudo systemctl enable --now tempo-beszel.

macOS (launchd)

If the host is a Mac, run the wrapper from a LaunchAgent. Create ~/Library/LaunchAgents/app.tempo.beszel.plist with ProgramArguments pointing at beszel-run.sh, RunAtLoad and KeepAlive set to true, then load it:

launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/app.tempo.beszel.plist
launchctl list | grep beszel

Windows

The poller is pure Python 3 and runs on Windows. Provide the secrets as environment variables (or *_FILE paths), then keep python beszel-tempo.py alive with a process supervisor: a Scheduled Task that runs at logon and restarts on failure, or a service wrapper such as NSSM or WinSW.

What you’ll see

Each event lands as one stack per host + metric, and the stack is stateful. A triggered alert and its later resolution update the same row rather than creating a second one, and flap cycles group together within a six-hour window.

Severity follows the alert, and a sender-provided severity wins:

Grouping and actions

Grouping keys on ${metadata.System}/${metadata.Metric}. A triggered alert opens or continues the stack, and a resolved alert closes it. The opening window is six hours, so a monitor that flaps several times in an afternoon reads as one stack with its history inside.

Two actions are attached to every event:

Troubleshooting and limitations