# Notifications _How pages reach people: channels and providers, ntfy, who gets them and how to acknowledge from the notification._ Back to the [README](../README.md) and the [documentation index](./README.md). A page reaches a person through their **channels**. A channel is one way of reaching them: a *provider* (the kind of thing that delivers — `ntfy` today) and an *address* that provider understands (an ntfy topic). A person can have several, and a page goes to **every channel that is switched on**, each queued and retried independently, so one provider being down does not hold back the others. ## Providers A provider is configured by the **server**, in its deployment, and is not something a user picks. Only the kinds the server has been configured with can be added as a channel: the Account page asks the server (`GET /api/notify/providers`) and offers just those. With none configured, notifications are off entirely: no goroutine, and nothing is ever queued. | Provider | Configured by | Address | Acknowledge button | |---|---|---|---| | `ntfy` | `TERDUT_NTFY_URL` (+ `TERDUT_NTFY_TOKEN`) | a topic | yes | A provider that cannot show a button gets the incident's link in the message body instead. Adding a provider is one Go type in `internal/notify` and a line in `cmd/terdut/main.go`; the notifier, queue, API and web UI do not change. ## Channels Everybody manages their own under *Account* in the web UI: add a channel, send a **test** through it (the point is that a phone buzzes, and only the person holding it can tell), turn it off, remove it. `GET|POST /api/users/{id}/channels` and `PATCH|DELETE .../{channelID}` are the same thing over the API, and an administrator may manage somebody else's, but never sees where they go: a channel's **address** is only sent to its owner, because it can be a secret. An account holds at most 10 channels. Turning a channel off is the off switch for it: pages skip it, including the reminders of an incident that already paged it. A user with no usable channel falls back to the install's fallback target (`TERDUT_NTFY_FALLBACK_TOPIC`), as does an incident that opens with nobody on call. If neither yields a target, nothing is queued. An ntfy topic is a shared secret with the ntfy server: anyone who knows it can both read the pages and publish to it, so an unguessable one is worth the trouble. That is also why an address never appears in an incident's timeline, which every API key can read — the timeline names the channel *type* only. Three things get sent: - **triggered** — an incident opened. Urgency follows severity (on ntfy, `critical` maps to max priority, the one that overrides the phone's quiet settings). - **reminder** — the incident is still `triggered` after `TERDUT_NOTIFY_REPEAT`. Repeats until somebody acts. Acknowledging, snoozing, resolving or archiving all stop it — snooze is the mute button. - **resolved** — every alert under the incident stopped firing. Only sent to whoever was paged in the first place, and only for the automatic cascade: resolving by hand pushes nothing, since the person who did it already knows. Notifications on a provider that can show one carry an **Acknowledge** button that acknowledges the incident without opening anything. It POSTs to `/api/notify/ack/{token}`, an unauthenticated route authorised by the 256-bit token in its path — minted fresh per notification, scoped to one incident and one action, and valid for 24 hours. A real API key is never put in a notification, because the message is stored on the provider's server and cached on the device. The token is **not** consumed by use. Acknowledging is idempotent, so a token stays valid for its full 24 hours and a second tap is a no-op that reports the incident's current state rather than an error — which is what you want when a tap is retried on a flaky mobile connection. What bounds it is scope, not a use count: one incident, one action, one day. Expired tokens are purged by the sweeper. Two consequences worth planning for: - `/api/notify/ack/{token}` **must stay publicly reachable**, or the button will not work when the responder is off your network. - Notifications sent to the fallback target carry **no** Acknowledge button. The target is shared, and a button on it would let any recipient acknowledge as somebody else. Delivery is a queue, not an inline call: the webhook writes a row per channel and a background notifier sends it within 30 seconds, retrying with exponential backoff up to 8 attempts. Nothing about ingestion blocks on a provider being reachable. Each row keeps a snapshot of its address, so a message already queued still goes out if the channel is edited or removed; later reminders and the all-clear follow the channels as they are then. Every delivery is recorded on the incident's timeline: a `notified` event once the provider accepts it (`triggered via ntfy`), and a `notify_failed` event when a notification exhausts its retries. Written from the result rather than at enqueue, so the timeline says what actually happened — and a page that never landed is visible instead of looking the same as one that did.