Skip to content

Notifications

The Notifications page is the configure surface for every outbound signal NullRun sends. It lives at /control-center/notifications in the sidebar and is gated by the Starter plan and above.

The page has three sections in this order:

  1. Channels — where signals can land (Slack / Email / Webhook).
  2. Alert rules — threshold rules that fire when a value crosses.
  3. Event subscriptions — which events reach which channels.

It is the configure surface for Alerts (the read surface); channels created here appear in the Alert rules editor, and alerts dismissed on the Alerts page keep their wire-side notification enabled.

Channels

The top section is a 2-up grid of channel cards. Each card carries:

  • Icon tile + Channel name.
  • Masked URL in mono for webhook channels; Slack channels show the channel name. See the Email variant note below.
  • Status dot — neutral for idle, faint for last_sent (so the operator can see at a glance whether the channel has fired recently).
  • Edit link + Send test icon-btn + On / off switch row.

Adding a channel

Click + Add channel in the section header. The dialog supports:

  • Slack — OAuth-branded setup with Slack-specific help text. Pre-pivot rows that store installation_id / channel_id in config are preserved on edit so the existing connection doesn't break.
  • Webhook — generic HTTPS POST. The signing secret is optional; if set, the receiver verifies X-NullRun-Signature (HMAC-SHA256, 5-minute clock-skew tolerance, 10-minute nonce replay defence).

Both Slack and generic webhook store on the backend as channel_type: "webhook" with config: { type: "webhook", url: ... } — Slack incoming webhooks accept POST JSON out of the box, so no Block Kit transform is needed for MVP.

Email variant

The Email channel type is not available in the dialog. The channel list may contain read-only rows from prior installs; new channels use Slack or Webhook.

Send test

The Send test button on each card posts a synthetic payload to the channel; the toast reports success or surfaces the backend's error message. Use this after creating or editing a channel to confirm your URL / OAuth installation actually delivers before relying on it for production signals.

Alert rules

The middle section is a list of threshold rules. Each rule renders as a card with:

  • Left-border accent by severity — info / warning / critical.
  • Inline gauge bar showing last_observed_value / threshold_cents live from the wire.
  • Last-fired timestamp + enabled toggle + delete link on the right.

Rule editing is a form inside an AlertRulesSection dialog. The form shape mirrors the wire contract — name, severity, threshold in cents, and which channels the rule routes to.

Event subscriptions matrix

The bottom section is the matrix that decides which event reaches which channel. Events are grouped by area:

  • Workflow activity — workflow.killed, workflow.paused, workflow.resumed, workflow.created.
  • Governance & access — approval.created, approval.decided, policy.changed, key.created, key.revoked.
  • Team — member.invited, member.joined, member.removed.
  • Digest — weekly spend digest, monthly quota report.

Each event is a row; each channel is a column. A cell shows a chip-dot when the event is enabled for that channel; no chip means the event is disabled for that channel. The right edge of each row has a master on/off toggle that flips every channel at once.

A footer legend explains the chip-dot semantics (● = enabled, no chip = disabled).

Plan gating

The Notifications page itself renders a TierGate upgrade prompt for plans without Starter; the sidebar link is also hidden. The upgrade card links to Billing & Plan (/control-center/billing) and to the public pricing page (which hosts the comparison table) so operators can inspect feature deltas before committing.

The plan-tier gate is enforced server-side on every /api/alert_channels and /api/alert_rules handler — a Lite user cannot POST to the API directly even if the page itself doesn't render.

API hooks

For automations, the same actions are exposed via REST:

  • GET /api/alert_channels / POST / PATCH /{id} / DELETE /{id}.
  • POST /api/alert_channels/{id}/test — fire a synthetic payload.
  • GET /api/alert_rules / POST / PATCH /{id} / DELETE /{id}.

The full endpoint catalog is in HTTP API → alert channels (and the alert-rules section, when split out).

  • Alerts — the read surface for what fired.
  • Audit log — every channel and rule mutation is recorded as an audit row.

Deep dive

Two channel types carry signals: Slack, and a generic webhook that posts JSON to any HTTPS receiver. Both receive the same body, so a Slack channel needs no transformation. A channel marked test-only is partitioned away before delivery, so synthetic test payloads never reach a production destination.

A webhook channel with a signing secret gets three headers: X-NullRun-Signature, X-NullRun-Timestamp, and X-NullRun-Nonce. The signature is an HMAC-SHA256 over the timestamp, the nonce, and the body joined in that order, and it is compared in constant time. A receiver should reject a timestamp that differs from its own clock by more than five minutes, and should remember a nonce for ten minutes. The nonce ledger expires on the same horizon as the accepted timestamp window, so it can never outlive a timestamp it exists to defend.

Delivery is throttled per organization and per detector, at one alert per fifteen minutes per bucket, which means a burst of different alerts sharing a detector collapses into one. Each organization can also opt out of individual event classes; the default is on, and a failure to read that setting is treated as opt-out rather than as a reason to amplify delivery during a dependency blip. A dispatch failure never blocks the producer that raised the alert, and a fan-out that does not land is picked up by a later pass.

Every alert is recorded for the Alerts page before delivery is attempted and before the channel list is consulted, so an organization with no channels configured still sees the full history of what fired.