Notifications
Out-of-app delivery for alerts. Enterprise plan. Configure at Settings → Notifications.
Pro includes in-app alert triage only. Upgrade to Enterprise for Slack, Teams, email, and signed webhooks.
See also: Scheduled compliance reports, CSV exports.
Channel kinds
| Kind | Secret format |
|---|---|
slack |
Slack incoming-webhook URL (plain text) |
teams |
Teams incoming-webhook URL (plain text) |
email |
JSON: { "api_url": "https://api.resend.com/emails", "api_key": "...", "from": "alerts@msp.com", "to": "oncall@msp.com" } |
webhook |
JSON: { "url": "https://...", "signing_secret": "..." } |
Secrets are stored in Supabase Vault and never re-displayed. To change a secret, delete the channel and re-create. Destination URLs must use public HTTPS, may not contain URL credentials, and are re-resolved immediately before each delivery. Redirects are not followed, which prevents a public endpoint from redirecting dispatch to an internal host. Channel secrets are limited to 32 KiB.
Filters
Per channel:
- Minimum severity -
info/warning/critical. An alert fires on this channel only if its severity ≥ minimum. - Kind allowlist - comma-separated list. Empty = all kinds. Use this to e.g. send only
breakglass.signinto a high-priority Slack channel.
How delivery works
The alert table has an INSERT trigger that fires the alert-dispatch Edge Function via pg_net. Dispatch reads every enabled channel for the alert's MSP, filters by severity + kind, and POSTs to each. Successes and failures are recorded in notification_delivery.
Each provider request has a 10-second timeout and a 256-KiB serialized-body limit. Provider response bodies are discarded without being buffered, and only stable error codes (never provider text or destination URLs) are written to the delivery log. Dispatch fails closed when an MSP has more than 100 configured channels instead of silently truncating fan-out.
Dedupe-merged alerts refresh the existing row's fired_at and intentionally
re-trigger dispatch. The delivery ID is stable for retries of one firing and
changes when fired_at changes, so generic webhook receivers can deduplicate
ambiguous retries without suppressing a later occurrence. Email API requests
also receive that value as their Idempotency-Key.
Signed webhooks
For the generic webhook kind, if signing_secret is in the JSON, every POST includes an X-Policytab-Signature header with the HMAC-SHA256 of the body (hex). Verify in your receiver:
const expected = crypto
.createHmac('sha256', SIGNING_SECRET)
.update(rawBody)
.digest('hex');
if (expected !== req.headers['x-policytab-signature']) reject();
Slack message format
*[critical] breakglass.signin* on _Tenant A_ at 2026-05-19T13:42:00Z
plus a code block with the alert payload (user, app, IP, status).
Teams message format
A MessageCard themed by severity colour: critical=red (#DC2626), warning=amber (#D97706), info=blue (#2563EB).