Notifications
The notification system tells you about events on projects you care about — scans finishing, gates failing, new CVEs landing on a component you depend on, approvals waiting, and license-policy violations. Notifications fan out across four channels (in-app, email, Slack, Microsoft Teams) and you decide globally which channels to receive on.
Any signed-in user. The header bell and /notifications page are visible to every role; admins additionally configure the SMTP / Slack / Teams transports under Disk & system health.
Notification triggers are wired into the producing services — scans, vulnerability ingest, license findings, the build gate, and the approval workflow all emit in-app rows (and external channels per your Preferences). See the full trigger catalog under Triggers.
The header bell
Every page has a bell icon in the top-right of the header. The badge shows the number of unread in-app notifications:
- 0 — no badge.
- 1–99 — exact count.
- 100+ — capped at 99+ so the badge does not break the layout.
Click the bell to navigate directly to the /notifications inbox. The bell does not surface a dropdown preview in this release — see Roadmap.
The Inbox at /notifications
/notifications is the full list of every notification you have received, newest first. Pages of 20 are loaded one at a time; use the Previous / Next controls at the bottom to walk through history.

Each row shows:
- Title — bold while unread.
- Body — one or two lines.
- Channel icons — which channels delivered this event for you (in-app always shows; email / Slack / Teams show only if you opted in).
- Timestamp — absolute on hover, relative otherwise.
- Mark read — a click anywhere on the row marks it read; the row dim-loads.
- Open — clicking navigates to the source resource and marks read on the way.
Bulk actions: Mark all as read (clears the unread badge for the current page).
Preferences
Below the inbox, the Preferences section lists every trigger with four global, per-channel toggles. The choice applies across every trigger — there is no per-trigger matrix in this release (see Roadmap).
| Channel | Default | Notes |
|---|---|---|
| In-app | on | The fallback channel. Always available; toggle still surfaces for symmetry. |
| on | Requires SMTP_* configured by the operator. | |
| Slack | off | Requires SLACK_WEBHOOK_URL configured. |
| Teams | off | Requires TEAMS_WEBHOOK_URL configured. |
Toggles enter a draft state. Click Save to persist your changes — the page tracks dirty state and disables Save until something changes.
Routing rules: who else hears
Your toggles decide what reaches you. A routing rule decides who else hears, and it is written by an administrator rather than by the person receiving it. The rule an organization actually has is usually of this shape: "the security team hears about every critical finding", or "release@ gets a line whenever a scan finishes in this one project".
A rule names some conditions and some destinations. Every condition is optional and an absent one matches everything:
- Kinds: which triggers it covers. Empty means all of them.
- Minimum severity: that severity and everything above it. A rule naming one does not fire for a notification that carries no severity at all, since "no severity" is the absence of an answer rather than a low one.
- Project: one project, and it must be a project the rule's scope covers.
And the destinations: channels to add, addresses to add, or both. A rule that names neither is refused, because a row that does nothing reads later as a row that is broken.
Team administrators write rules for their own team at
POST /v1/notification-rules/teams/{team_id}; a super admin writes rules for
the whole deployment at /v1/notification-rules/org/{organization_id}. Reading
is open to everyone in the team, so people can see what reaches them without
being able to change it. A team's list includes the organization's rules,
because those apply to the team too.
Every rule whose condition matches contributes, and the results are combined. A rule cannot remove a recipient or switch off a channel somebody enabled: two mechanisms deciding one question would mean the silencing one wins an argument nobody had, and a person whose notifications quietly stopped would have no way to find out why.
Write no rules and nothing changes. Every notification goes exactly where it went before.
How fresh is the bell?
The frontend polls the unread count every 60 seconds while the tab is active. Two optimisations keep the channel lean:
- When the browser tab is hidden (Page Visibility API reports
hidden), polling pauses to conserve battery and server bandwidth. - When the tab regains focus, the frontend fires an immediate poll so the badge catches up before the next 60-second tick.
If you have multiple portal tabs open, each polls independently — the unread state is server-authoritative, so the counts converge.
Triggers
Eight distinct triggers fire notifications:
| Trigger | When it fires |
|---|---|
scan_completed | A scan you started, or one on a project you watch, finishes successfully. |
scan_failed | A scan you started, or one on a project you watch, fails. |
cve_detected | A new CVE lands on a component already present in one of your scans (DT NVD ingest correlates against existing components). |
license_violation | A scan surfaces a forbidden-license component on a project you watch. |
approval_pending | A component requires approval. In this release the notification is delivered to all super-admins in the org (no per-team approver routing); a designated-approver model is on the roadmap. |
approval_state_changed | An approval request you filed changes state (approved / rejected / moved to review) — the requester is notified of the disposition. |
policy_gate_failed | A CI build gate fails (Critical CVE or forbidden license blocks the build). |
vuln_sla_breach | Open findings on a project's latest succeeded scan crossed their remediation-SLA due date within the last 24 hours (daily sweep at 02:45 UTC). One aggregated alert per project, delivered to every member of the owning team, with a link to the Vulnerabilities tab pre-filtered to overdue. See Remediation SLA and aging. |
malicious_detected | A component already present in a project's latest succeeded scan was named by a newly published malicious-package advisory (weekly re-check, Sundays 02:40 UTC). No scan produces this finding — nobody re-scans an old release. Delivered to every member of the owning team. Treat it as an incident, not an upgrade: the published artifact is the attack, so remove it and rotate any credential the build had access to. See Malicious packages. |
Channel selection is global — the Preferences tab decides which channels deliver every trigger. Two exceptions: vuln_sla_breach and malicious_detected are delivered in-app only (your in-app preference is still respected); it never goes out via email, Slack, or Teams.
Verify it worked
- Trigger a scan on a project you own; within seconds of completion, the bell badge increments and
/notificationsshows the new row.
- Open
/notificationsin a second tab and mark a row read; the first tab's bell badge decrements within 60 seconds.
- Globally disable email under Preferences and click Save; run another scan and confirm the next
scan_completednotification arrives via in-app only.
Troubleshooting
- Bell badge never updates — the tab may be hidden behind another window. Bring it to the foreground; the immediate poll on focus refreshes the count.
- Email never arrives — verify the operator has configured SMTP and that the destination address is your verified email (visible on
/profile). - Slack message never arrives — confirm the operator has set
SLACK_WEBHOOK_URLand that the channel still exists. Slack returns 404 silently when a webhook is revoked.
Roadmap
Items the manual previously promised that are not in this release; tracked for later releases.
- Header-bell dropdown with the five most recent notifications and a "go to inbox" footer — planned; today the bell navigates straight to
/notifications. - Infinite scroll on
/notifications— planned; today the inbox uses Previous / Next pagination. - Per-trigger × per-channel preference matrix (e.g. Slack only for
policy_gate_failed) — planned; today the channel choice is global across all triggers. disk_pressurenotification trigger for admins — planned; the disk-pressure event is currently surfaced only on the admin dashboard.
See also
- Authentication & profile — your verified email is the destination for email notifications.
- Integrations — webhook secrets are separate from notification channels.
- Disk & system health — operator setup for SMTP / Slack / Teams.