edwh-restic-ntfy
ntfy notifications for edwh-restic-plugin: a failed backup reaches your phone instead of dying in a cron log.
Core emits an event per restic operation and ships no notifiers itself. This package is one — it
registers a channel called ntfy, and does nothing at all until [restic.notify] channels names it.
Installation
(uv) pip install edwh-restic-ntfy
Installed beside edwh-restic-plugin, it is discovered through its entry point; no import or module
list is needed.
Quick start
.env (gitignored, secrets only):
NTFY_TOKEN=tk_your_token_here
.toml (or default.toml, the committed template):
[restic.notify]
channels = ["ntfy"]
[restic.notify.ntfy]
url = "https://ntfy.sh/acme-backups"
Then check it works without waiting for a real failure:
edwh restic.notify-test
edwh restic.notify-test --all-events
A public ntfy topic needs no credentials, so NTFY_TOKEN is optional. A url is what activates the
channel: named in channels but without one, it is skipped with a note and your backups run on.
Routing: one url, or one per anything
url above is shorthand. The general form is a table whose keys say which events go where:
[restic.notify.ntfy]
server = "https://ntfy.example.com" # optional, default https://ntfy.sh
[restic.notify.ntfy.urls]
"*" = "acme-all" # everything, unless a key below claims it
"backup" = "acme-backups" # one operation (also spelled "backup.*")
"warning" = "acme-noisy" # one level (info | warning | error)
"failed" = "acme-oncall" # one phase (also spelled "*.failed")
"check.failed" = "https://ntfy.sh/acme-damage" # one combination
Five kinds of key, freely mixed:
| Key | Means | Matches |
|---|---|---|
"*" |
everything | any event |
"backup", "backup.*" |
one operation | backup.started, backup.failed, … |
"failed", "*.failed" |
one phase, across operations | backup.failed, check.failed, … |
"error", "warning", "info" |
one level | whatever core gives that level |
"check.failed" |
one exact event | only that one |
Operations are backup, restore, check, forget, wipe; phases are started, succeeded,
failed, slow. A key that could never match anything — "faild", "nonsense.failed" — is
reported and skipped when the channel is configured, so a typo tells you at once instead of quietly
never firing.
Which url wins
The most specific key wins, and exactly one url set is chosen per event:
"check.failed" (exact) > "failed" (phase) > "error" (level)
> "backup" (operation) > "*"
Overlapping keys never fan out, so a single failure cannot wake someone twice, and "*" behaves as
the fallback rather than as an extra copy of everything.
Phase and level deliberately outrank operation. Routing by severity is the whole reason to have a
second url, and a "backup" key that swallowed backup.failed away from your oncall topic would
defeat it. Between the two, phase beats level, because it is the narrower statement.
When you do want one event in two places, say so with a list:
[restic.notify.ntfy.urls]
"failed" = ["acme-oncall", "https://ntfy.sh/acme-audit"]
Both are attempted; one dead host does not cost the other its notification.
Topics and urls
A value containing :// is used as it is. Anything else is a bare topic, resolved against server
(default https://ntfy.sh, overridable with NTFY_SERVER in .env). Mixing is fine — that is how
one channel reaches both a self-hosted server and ntfy.sh.
Credentials
.env key |
Purpose |
|---|---|
NTFY_TOKEN |
access token, sent as Authorization: Bearer to every host |
NTFY_TOKEN_<HOST> |
overrides NTFY_TOKEN for one host |
NTFY_USER, NTFY_PASSWORD |
basic auth, used when no token applies |
NTFY_SERVER |
default server for bare topics |
<HOST> is the hostname uppercased with everything non-alphanumeric replaced by _:
ntfy.sh → NTFY_TOKEN_NTFY_SH, ntfy.example.com:8080 → NTFY_TOKEN_NTFY_EXAMPLE_COM_8080.
Core hands this plugin only the NTFY_* keys from .env, so restic's password and other channels'
tokens are never in scope.
What a notification looks like
Title, then body:
backup.failed - acme-prod@db-01
backup failed with exit 2 after 4m12s
repository: s3:acme
host: db-01
project: acme-prod
target: files
failed scripts: backup_files.sh (exit 1)
Fatal: unable to open repository at s3:...
The phase picks the icon (⏳ started, ✅ succeeded, 🚨 failed, 🐌 slow) and the level picks the ntfy
priority: info → 3, warning → 4, error → 5, so only bad news pushes past a normal
notification.
Override priorities with the same keys and precedence as urls:
[restic.notify.ntfy]
priority = 4 # one value for every event
[restic.notify.ntfy.priorities] # or per route
"*" = 2
"backup.failed" = 5
All options
Under [restic.notify.ntfy]:
| Key | Default | Meaning |
|---|---|---|
url, topic |
— | shorthand for the "*" route |
urls, topics |
— | the route table (or a bare value, same as url) |
server |
https://ntfy.sh |
base for bare topics |
priority / priorities |
per level | 1–5, single value or route table |
logs |
true |
include restic's stdout/stderr on failures |
timeout |
4.0 |
seconds per request |
max_bytes |
3800 |
message budget; longer bodies are cut with [truncated] |
events and min_level are read by core, not by this plugin, and still apply:
[restic.notify.ntfy]
url = "https://ntfy.sh/acme-backups"
min_level = "warning" # core drops info events before routing ever sees them
You rarely need events: this plugin narrows core's subscription to what its own table can deliver,
so restic.notify-test reports an event with no matching route as filtered rather than as a silent
success. An event that reaches us with no route sends nothing and says so once per event name.
A worked setup
[restic.notify]
channels = ["ntfy"]
project = "acme-prod-db01"
[restic.notify.ntfy]
server = "https://ntfy.example.com"
logs = true
[restic.notify.ntfy.urls]
"*" = "backups-log" # the timeline: started, succeeded, forget, …
"failed" = "backups-oncall" # anything broken, whichever operation
"check.failed" = "backups-urgent" # repository damage gets its own topic
"backup.slow" = "backups-log" # a slow backup is not worth a phone call
[restic.notify.ntfy.priorities]
"check.failed" = 5
"*" = 2 # the timeline should not buzz
Security note
A notifier runs in-process and is trusted like any other dependency. Events carry an allowlist of
fields and never the repository URI — but logs on a failure event is restic's full stdout/stderr,
which can include repository paths and hostnames. Point failure routes at a topic you would be
comfortable pasting a terminal session into, protect it with a token, or set logs = false for that
channel.
Development
uv pip install -e .[dev]
edwh test.run
edwh plugin.release # to publish a new version with vommit
License
edwh-restic-ntfy is distributed under the terms of the MIT license.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file edwh_restic_ntfy-0.1.0.tar.gz.
File metadata
- Download URL: edwh_restic_ntfy-0.1.0.tar.gz
- Upload date:
- Size: 12.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
uv/0.11.16 {"installer":{"name":"uv","version":"0.11.16","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Linux Mint","version":"22.3","id":"zena","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ce0c2e1d300d27bffbba2a87241a4ce3d9df1ea8fa3a0f80e5f646370d7cac4a
|
|
| MD5 |
1885f2283fcc3c4b5d52e9fe4e3277b4
|
|
| BLAKE2b-256 |
da9221247dac9e5ec9200bfdeb7d2f5d77a7c61dc5844801792aa0015887f36e
|
File details
Details for the file edwh_restic_ntfy-0.1.0-py3-none-any.whl.
File metadata
- Download URL: edwh_restic_ntfy-0.1.0-py3-none-any.whl
- Upload date:
- Size: 14.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
uv/0.11.16 {"installer":{"name":"uv","version":"0.11.16","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Linux Mint","version":"22.3","id":"zena","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
33659accd3f727d40f2f6d3acd1ccbcaf57e181ee1f918ff7e3070d110bf4e9c
|
|
| MD5 |
54c8318c8ce1103ee980384268da9bc4
|
|
| BLAKE2b-256 |
cf2259142de3e3bbd18b14a9a54ecb4287b2fccfc703b1919b5e2e521db629eb
|