Notifications
A channel is a destination — an SMTP server, a Slack webhook, a Discord webhook, or any HTTP URL you control. You create channels once and wire them to as many endpoints as you like.
Channel types
Channels are managed under Notifications in the sidebar. The page surfaces:
- A grid of every channel with its connectivity dot, type, and quick Test action.
- The recent Delivery log — every dispatch attempt, success or failure.
- An Escalations card — escalation isn’t dispatched today; see Incidents → What’s not in the product.
- A Mutes panel for
endpoint/channel/globalmute rows — see Muting below.
When notifications fire
The check engine doesn’t notify on every failed probe. It opens an incident when the failure streak hits the endpoint’s threshold, then dispatches based on incident state changes:
| Kind | Severity | When |
|---|---|---|
incident_opened | critical if cause is endpoint_down, else warning | Incident transitions from nothing to active. |
incident_resolved | success | Incident transitions from active to resolved. |
channel_test | info | You click Test on a channel. |
incident_opened and incident_resolved are triggered by a Supabase Database Webhook — the trigger fires the moment the incident row is written, so first-message latency is bounded by the webhook round-trip plus the channel provider.
Routing — wiring channels to endpoints
Routing is set per-endpoint, not per-channel. Open an endpoint and switch to its Notifications tab. You’ll see every channel you’ve created with two switches:
- Wired — toggle whether this endpoint’s incidents are dispatched to this channel.
- Paused — temporarily silence this channel for this endpoint only without unwiring it.
Wired-but-paused gets stored in paused_notification_channel_ids on the endpoint row. The dispatcher computes channelsToFire = notification_channel_ids \ paused_notification_channel_ids on every dispatch.
What gates a dispatch
For each (incident, channel) pair, the dispatcher walks these gates in order. The first one that says “skip” stops processing and writes a delivery_status='suppressed' row to the log with the reason.
- Endpoint pause — channel id is in the endpoint’s
paused_notification_channel_ids. - Channel disabled — the channel’s
enabledflag is false. - Recovery alert off — the dispatch is
incident_resolvedand the endpoint’srecovery_alertis false. (Short-circuits without writing a log row.) - Global mute — the user’s
global_mute_untiltimestamp is in the future. - Severity filter — see below.
- Event filter — see below.
If everything passes, the channel’s provider.send() runs and the result (sent / failed) is logged.
Severity filter
Each channel has a severityFilter:
| Value | Lets through |
|---|---|
info+ | Everything (info, warning, critical). |
warning+ | warning and critical. |
critical | critical only. |
Recovery dispatches (success severity) bypass the filter — once an open incident exists and you’re getting paged, you’ll always get the all-clear too (assuming recovery_alert is on).
Event filter
Per-channel eventFilters toggles:
sendOpen— fire onincident_opened.sendResolved— fire onincident_resolved.
Both default true. Switch off sendOpen for a channel that should only send the all-clear; switch off sendResolved for a channel that should only page on opens.
Muting
Two systems coexist; only one is wired to dispatch today.
Global mute (works)
Set under Settings → Notification preferences. While global_mute_until is in the future, every dispatch is suppressed and a log row is written with suppressed_reason='muted'. Use it to silence everything during a planned change window.
Scoped mutes (UI-only today)
The Mutes panel on the Notifications page lets you create endpoint / channel / global scoped mute rows. The rows are visible and deletable from the UI.
Scoped (endpoint / channel) mute rows are stored but not enforced by the dispatcher today. Only global muting via global_mute_until actually suppresses messages. To silence one endpoint, use the Pause toggle on the endpoint’s Notifications tab. To silence one channel everywhere, set the channel’s enabled flag to false.
Connectivity status
The dot on each channel card reflects is_connected:
- Test updates
last_tested_at, pluslast_success_atandis_connected=trueon success, orlast_failure_atandis_connected=falseon failure. - Live dispatches do not update these columns. A channel can be sending happily for weeks and still show as last-tested-some-time-ago. Use the Delivery log to verify live behaviour.
Stored-but-inert fields (heads-up)
The channel form includes these fields, but the dispatcher does not consult them today. Setting them is harmless — just don’t rely on the behaviour they suggest:
- Delivery priority (
standard/critical) — UI label says “Critical bypasses cooldown and rate limits”; neither cooldown nor rate-limits are enforced. - Quiet hours — JSONB column exists, dispatcher never reads it.
- Rate limit —
maxPerMinutevalue is stored but never gated. - Retry on failure — flag is stored, no retry path is implemented.
These will move from “stored” to “enforced” over time; for now, treat them as planned.
Quotas
Notification channel counts are capped per plan. The Add Channel button disables at the cap; over-cap inserts return Postgres error PT403. See Pricing for the per-plan numbers and Limits for the broader product caps.
Notification log retention
Every dispatch attempt is written to mx_notification_log. Rows are pruned at 90 days by a daily cleanup cron — older delivery history disappears. The incident’s notifications_sent counter is incremented per delivery so the count survives the prune even when individual rows do not.