Chapter 18 of 26
Notification Destinations
What a destination is
A destination is an endpoint that outgoing notifications are sent to: an SMTP mailbox, an HTTP webhook URL, an ntfy topic, or the built-in Internal Alerts destination that records alerts inside PatchMon itself.
Destinations are the "where" half of the notifications pipeline. The "what goes there" half is handled by event rules, covered in Notification Routes and Delivery Log.
Destinations live under Reporting → Destinations in the web UI.
Channel types
PatchMon 2.0 ships four destination channel types. The list is fixed in the server code:
| Channel | Value | What it does |
|---|---|---|
| Webhook | webhook |
HTTP POST of a JSON payload to any URL. Generic by default; Discord and Slack webhook URLs are auto-detected and formatted with the appropriate rich payload. |
email |
SMTP delivery to one or more recipients, with HTML body and an optional attachment for scheduled reports. | |
| ntfy | ntfy |
Push notification via ntfy.sh or a self-hosted ntfy server. |
| Internal Alerts | internal |
Built-in destination that drops events into the Alerts tab. You cannot create or delete this one; it is created automatically and can only be enabled or disabled. |
Discord is a webhook, not a channel type. To post alerts to a Discord channel, create a Webhook destination with the Discord webhook URL. The separate Settings → Discord Authentication area is only for Discord OAuth2 sign-in; it is unrelated to notifications.
Permissions
Creating, editing, testing, and deleting destinations requires the can_manage_notifications permission. Admins and superadmins bypass the check. Users without the permission do not see the Destinations tab at all.
Creating a destination
- Open Reporting → Destinations.
- Click Add destination.
- Pick a channel type (Webhook, Email, or ntfy) and click Next.
- Give the destination a Display name: this is what appears in the event rules picker, the delivery log, and scheduled report selectors.
- Fill in the channel-specific configuration (see below).
- Leave Enabled on (default) or turn it off to save the configuration without sending anything yet.
- Click Create.
A successfully-created destination shows up in the destinations table with its channel icon, display name, and enabled switch.
Webhook
Pick this for generic JSON webhooks, Discord, or Slack. Discord and Slack URLs are auto-detected and sent rich payloads; other URLs receive a generic JSON body.
| Field | Required | Notes |
|---|---|---|
| Webhook URL | Yes | Full HTTPS URL. Discord: https://discord.com/api/webhooks/.... Slack: https://hooks.slack.com/services/.... Generic: any endpoint that accepts POST with Content-Type: application/json. |
| Signing secret | No | Optional HMAC secret. When set, each webhook is signed with SHA-256 over the payload; the signature is sent in a header so the receiver can verify authenticity. |
Email (SMTP)
| Field | Required | Notes |
|---|---|---|
| SMTP host | Yes | e.g. smtp.example.com, smtp.sendgrid.net. |
| SMTP port | No | Defaults to 587. Use 465 for implicit TLS, 25 for unencrypted relay (avoid). |
| Username | No | SMTP auth user. Leave blank if your relay does not require it. |
| Password | No | SMTP auth password. Stored encrypted. |
| From | Yes | Envelope + header From address, e.g. patchmon@example.com. Must be accepted by the relay. |
| To | Yes | Comma-separated list of recipients. |
| TLS mode | Yes | Choose how the SMTP transport secures the connection. See TLS modes below. Defaults to STARTTLS for new destinations. |
TLS modes
PatchMon offers four TLS modes on every email destination. Pick the one your relay actually supports rather than leaving it on Auto, so a misconfigured server fails closed instead of silently downgrading to plaintext.
- STARTTLS (recommended). PatchMon connects in plaintext on the SMTP port (typically 587) and then requires the server to advertise
STARTTLS. The connection is upgraded to TLS before any credentials or message body are sent. If the server does not advertiseSTARTTLS, PatchMon refuses to send and reports the failure. This is the right choice for the vast majority of modern relays (Microsoft 365, Google Workspace, SendGrid, Postmark, Mailgun, Amazon SES on port 587, and most on-prem mail servers). - Implicit TLS / SSL. PatchMon opens a TLS connection from the very first byte, with no plaintext handshake. The default port for this mode is 465. Use it when your relay only accepts TLS on a dedicated port and does not support
STARTTLS. Some legacy or appliance-based servers only offer this mode. - None (insecure). Cleartext SMTP, no TLS at any stage. PatchMon refuses to send if a username or password is set on the destination, because it would otherwise leak credentials onto the wire. Use only for trusted internal relays on a private network where TLS is genuinely unavailable.
- Auto. Legacy opportunistic mode kept for backward compatibility. PatchMon tries
STARTTLSfirst and falls back to implicit TLS on the same host and port ifSTARTTLSis not advertised. Existing destinations that were saved before the explicit modes were added load as Auto so they keep working unchanged. Open the destination, pick STARTTLS or Implicit TLS / SSL explicitly once you have confirmed which one your relay supports, and save. New destinations should not be configured as Auto.
Port and mode are independent. The port field is just the TCP port to connect to; the TLS mode controls how the connection is secured. The defaults (587 for STARTTLS, 465 for implicit TLS) match the conventional ports, but you can override the port if your relay listens elsewhere.
Send test email
Saved email destinations have a Send test email button next to the standard Test action. Unlike Test, which enqueues a synthetic notification through the worker, Send test email performs a synchronous live SMTP probe directly from the API request and reports the result inline:
- On success the toast confirms delivery and the recipients should receive a short test message.
- On failure PatchMon reports which stage of the SMTP exchange failed:
validate(the configuration is rejected before any network activity, for example a missing host or a username set with TLS mode None),dial(the TCP connection or implicit TLS handshake could not be established),starttls(the server did not advertiseSTARTTLSin the chosen mode),auth(the relay rejected the credentials), orsend(the relay accepted the session but rejected the recipients or message). The toast includes the underlying error message returned by the relay.
This is the fastest way to diagnose a TLS or auth misconfiguration without trawling through server logs. The probe respects the same can_manage_notifications permission as editing the destination.
ntfy
| Field | Required | Notes |
|---|---|---|
| Server URL | No | Leave empty for https://ntfy.sh. Fill in your own URL for self-hosted ntfy. |
| Topic | Yes | ntfy topic name. Subscribe to the same topic on your phone/desktop to receive push notifications. |
| Access token | No | ntfy bearer token for protected topics. Alternative to basic auth. |
| Username / Password | No | Basic auth. Use instead of access token when your ntfy server is configured for HTTP basic auth. |
Internal Alerts
You cannot create this destination; it is seeded automatically with the ID internal-alerts and appears with the Built-in tag. Its sole job is to write events into the in-app Alerts tab. You can:
- Enable / disable it from the destinations table (disable if you do not want internal alert records at all, for example when you only use external chat or email).
- Reference it from event rules, same as any other destination.
You cannot delete it. Attempting to delete returns 400 Bad Request: The Internal Alerts destination cannot be deleted. You can disable it instead.
Editing a destination
Click Edit in the destinations table. The modal re-loads the current configuration (secrets included, so you do not have to re-type passwords or tokens) and lets you change any field. Click Save to apply.
The enabled switch is inline in the table; click it to toggle without opening the modal.
Secrets are always encrypted at rest using PatchMon's SESSION_SECRET. When you re-enter a secret and save, the value is re-encrypted. The decrypted value is returned only to operators with can_manage_notifications.
Testing a destination
Use Test in the destinations row to verify the configuration without waiting for a real event:
- Click Test next to any enabled non-built-in destination.
- PatchMon enqueues a synthetic event with type
test, severityinformational, and the message "This is a test message from PatchMon notification settings." - A toast confirms the test is enqueued. Actual delivery happens through the notifications worker and takes a second or two.
- The Delivery Log updates automatically after about three seconds; look there for the outcome.
Tests do not bypass global rate-limiting. If your destination is already at its per-minute rate cap (60 messages/minute), the test returns
429 Too many notifications; try again shortly.
Failure cases returned by the test endpoint:
| HTTP | Message | Meaning |
|---|---|---|
400 Bad Request |
Destination is disabled |
Enable it first. |
404 Not Found |
Destination not found |
The destination was likely deleted in a parallel tab. |
429 Too Many Requests |
Too many notifications; try again shortly |
Rate limit hit. |
503 Service Unavailable |
Notifications not configured |
Background worker or Redis is not running. |
Deleting a destination
Click the trash icon in the destinations table. The confirmation dialog warns that the action is permanent. Deleting a destination does not delete the event rules that target it: those routes will be orphaned and should be updated to point at a different destination or removed. Deliveries in the Delivery Log keep their historical destination_id and appear as a raw ID if the name can no longer be resolved.
You cannot delete the internal-alerts destination (see above).
What gets stored
Every destination is a database row with:
- A stable UUID (
id) used by event rules and the delivery log. channel_typefrom the list above.display_name.enabledboolean.config_encrypted: the JSON configuration, encrypted withSESSION_SECRETso a database dump does not expose SMTP passwords, ntfy tokens, or HMAC secrets.created_at/updated_attimestamps.
The list endpoint never returns the raw config, only a has_secret flag. The decrypted config is fetched on demand from a separate endpoint when the edit modal opens.
Discord sign-in vs Discord webhooks
The two "Discord" areas in PatchMon are independent:
| Area | Purpose | Where |
|---|---|---|
| Discord Authentication | OAuth2 sign-in, users log in to PatchMon with their Discord account, optionally requiring membership of a server and role. | Settings → Discord Authentication |
| Discord webhook destination | Post alerts into a Discord channel via a channel webhook URL. | Reporting → Destinations → Add destination → Webhook |
Configure them separately. The OAuth2 settings are not required to send alerts to Discord.