Notifications
One sentence first, because everything else on this page exists to make it true: when Vigil decides to alert you, that decision is durable, and the message is delivered at least once or recorded as failed where you can see it.
25 native provider types, unlimited channels
Two different numbers, and they are worth keeping apart. 25 native provider types ship, in both editions, and that count is generated from the code so no page here can drift from it. Native means Vigil implements that service’s own documented API and pins the version. How many channels you configure is not limited at all: forty Slack channels, one per client, each pointing somewhere different, is a supported setup, and so are two pointing at the same place. Nothing counts them and no edition changes that.
There is also one bridge, and it is not a twenty-sixth integration. The Apprise provider forwards to an Apprise API server you run; what that server reaches is between you and Apprise. Vigil hosts no Apprise server, operates no shared relay, and has not tested the services yours forwards to. So the sentence is always “25 native providers, plus additional services through your own Apprise server”, and the two halves are never added together.
“Unlimited” means this application imposes no count. It is not a throughput promise: Slack and the rest still enforce their own rate limits, your server is still your server, and what a large fan-out actually costs is measured and published rather than glossed over.
A channel is a provider, its settings, its encrypted credentials and the event classes it subscribes to. Every provider rides the same transactional outbox: durable enqueue in the transaction that decided to alert, leases, retries with jittered backoff, idempotency keys derived from the cause, and a per-channel rate limit. No provider has a queue of its own.
| Provider | API Vigil is written against | Credential you supply |
|---|---|---|
| Chat | ||
| Slack | Incoming Webhooks | the webhook URL |
| Discord | Webhooks (API v10) | the webhook URL |
| Microsoft Teams | Workflows (Power Automate) trigger | the workflow URL |
| Telegram | Bot API sendMessage | bot token and chat id |
| Google Chat | Chat API v1 incoming webhook | the webhook URL |
| Mattermost | REST API v4, POST /api/v4/posts | bot token and channel id |
| Rocket.Chat | REST API v1, chat.postMessage | personal access token and user id |
| Matrix | Client-Server API v3 | access token and room id |
| Zulip | REST API v1, POST /api/v1/messages | bot email and API key |
| LINE | Messaging API v2 push | channel access token and destination id |
| On-call and tickets | ||
| PagerDuty | Events API v2, trigger and resolve | Events API v2 integration key |
| Jira Service Management | Jira Cloud REST API v3 | account email and API token |
| Push | ||
| Pushover | Messages API, /1/messages.json | application token and user or group key |
| Gotify | Server API, POST /message | application token |
| ntfy | Publish API, ntfy.sh or your server | token or user + password, optional |
| Pushbullet | API v2, POST /v2/pushes | access token |
| Bark | Bark API v2, POST /push | device key and your Bark server |
| Web Push | RFC 8291 aes128gcm, RFC 8292 VAPID | VAPID key pair and a browser subscription |
| Home Assistant | REST API, notify domain only | long-lived access token |
| SMS and messaging | ||
| Twilio SMS | Programmable Messaging 2010-04-01 | your Twilio account and a sender |
| Twilio WhatsApp | Programmable Messaging 2010-04-01 | a WhatsApp sender, and a template for out-of-hours |
| SMTP | Own client: STARTTLS/TLS, AUTH PLAIN | your mail server, password optional |
| Resend | API v1, POST /emails with Idempotency-Key | API key |
| Webhooks and buses | ||
| Webhook | Signed JSON POST, X-Vigil-Signature | HMAC signing secret |
| Amazon SNS | Query API 2010-03-31, Signature Version 4 | access key allowed to sns:Publish |
| Bridge, not an integration | ||
| Apprise (your own server) | apprise-api, POST /notify or /notify/{key} | your own Apprise server |
Every credential is yours. There is no Vigil-funded relay, account, API budget or hosted service behind any provider, and that includes the bridge - there is no managed Apprise. Self-hosted rows (Gotify, ntfy, Matrix, Mattermost, Rocket.Chat, Zulip, Bark, Home Assistant, Apprise, the signed webhook) may point at servers on your own network; cloud metadata and link-local space are refused wherever a URL points or resolves, and redirects are never followed.
Rows worth reading twice. Teams is the Power Automate Workflows webhook - Microsoft disabled the old Office 365 connector URLs in May 2026, and Vigil never speaks the retired MessageCard format. Jira Service Management is not Opsgenie: Atlassian shuts Opsgenie down on 5 April 2027, so this is the Jira Cloud platform API, opening one issue per outage and closing it on recovery. LINE is the Messaging API, not the discontinued LINE Notify. Twilio WhatsApp needs an approved template for alerts outside a 24-hour service window, which is exactly when a 3am outage fires. PagerDuty is sent trigger and resolve and never acknowledge, because Vigil does not know whether a human picked the alert up. Amazon SNS is signed in-tree with Signature Version 4 and Web Push is encrypted in-tree per RFC 8291, both so the request goes through the same egress guard as everything else rather than through an SDK's own HTTP stack.
Routing
A channel subscribes to event classes, and every event belongs to exactly one, so one logical event cannot reach the same channel twice: monitor down/up, incident lifecycle, certificate and domain expiry (a carve-out of down/up by monitor kind - the certificate channel hears about certificates, the 3am pager does not), and in the commercial edition recovery results and remote-probe quorum. Recovery notifies outcomes only: success once verified, failure once the chain is exhausted. Quorum events fire on the transition, not once per round.
A channel also has a scope. Left alone it is a workspace default and hears about every monitor, plus the events that belong to no monitor at all, such as an incident somebody reported by hand. Point it at specific monitors and it hears about those and nothing else - which is how one Slack workspace gets everything and another gets only the client it belongs to. A channel matched by several rules is still sent one message.
Credentials and errors
Channel secrets are stored encrypted (AES-256-GCM) and are never sent back to a browser; the editor only shows which secret fields are set. Delivery errors are scrubbed of secret values and URL query strings before they are stored, and the delivery history shows provider, event, destination, attempts, final status and that redacted error. SMTP refuses to authenticate without TLS and certificate verification has no off switch.
Retries, honestly
Delivery is at-least-once, and since 1.18.0 it is durable: twenty attempts over a six-hour horizon, backed off exponentially to a half-hour ceiling and jittered a quarter either way. A provider that is down for an afternoon no longer costs you the alerts queued behind it — before 1.18.0 the whole retry window was 31 to 62 seconds, and an outage longer than a minute ended with every queued message marked failed. The schedule lives on the row, so a restart or a redeploy mid-outage changes nothing about when a message is next tried.
Where a chain ends, it says which bound it hit. Rejected means a provider refused it and will keep refusing — a revoked credential, an address that does not exist — and it is marked immediately instead of hiding behind a queue that never drains. Dead letter means it ran out of attempts. Expired means it ran out of time. A screen full of expired means a provider was unreachable; a screen full of rejected means something is misconfigured, and the two need opposite fixes.
Every attempt leaves its own record, written before anything is sent and never overwritten by a later one, so the settings page can show the whole timeline: attempt number, outcome, HTTP status, the redacted error and what the provider asked for. The unavoidable window is a crash after a provider accepted a message but before the row records it — that attempt is recorded as unknown rather than guessed at, and unknown attempts are counted separately, because they are the only case where a duplicate at the far end cannot be ruled out. Where the provider offers idempotency, the retry collapses on their side. The wording the product uses everywhere: at most one message per logical notification where the provider cooperates, at least one everywhere — and never a claim of exactly-once.
Finished deliveries can be replayed from the settings page, which queues new work and never rewinds the original: the record of what happened the first time is the point of having it. Terminal rows and their evidence are removed after thirty days; nothing queued is ever deleted, because a late message is not an old one.
The full engineering document, including the outbox schema and the
exact guarantee wording, ships in the repository as
docs/NOTIFICATIONS.md.