Skip to main content
Version: v5.7.0

Notifications

Included from Basic tierpacks:security-governance

The notification system is interest-based: subscriptions and mandates decide who hears about what, connectors decide how it reaches them. A server-side engine consumes the events stream, matches each event against the declared interests (plus two zero-config defaults: your own runs reaching a terminal state, and approvals waiting on you — a step wait gate naming you, or an approval checklist your line is on), re-checks the subscriber's read authorisation at match and dispatch time, and persists one notification row per (user, event) — the in-app feed row and the delivery-evidence record in one. The user plane is NotificationService, the admin/governance plane is NotificationAdminService (both on opschain docs api-v2); the field-by-field resource shapes are opschain docs resources/notification and opschain docs resources/notification-connector, and the governance runbook is opschain learn governance-notifications. This page is the component map: what each piece is, who administers it, and where the licence gates sit.

The engine​

A cursor-based consumer of the events stream (opschain docs events) drives match → authorise → group → render → deliver → record on every replica. The properties that matter operationally:

  • Authorisation twice. The matcher intersects candidates with the subscriber's read authorisation at match time, and dispatch re-checks against a fresh snapshot — a revocation between the two records a suppressed evidence row instead of a delivery.
  • Exactly-once. One row per (user, event) is database-enforced, so concurrent sweepers across an HA cluster converge; the cursor starts at the newest event on first boot — history is never replayed against interests declared later.
  • Redaction by construction. Summaries render from an allowlisted field set only (event type, classification, node path, action path, actor) — never event payload fields — so resolved secrets and PII cannot enter a notification; the deep link carries the reader to the authorisation-checked subject instead of embedding detail.
  • Noise controls. Every row is stamped with its resolved cadence (default, realtime, hourly, daily — the default posture is realtime for failures/errors and approvals-waiting, daily digest for the rest); the in-app row always lands realtime, while a connector-routed row's egress waits for its digest window. Thread grouping keeps one updating row per change or workflow run, and a failure flood collapses to one row naming the blast radius. The mechanics are on opschain docs resources/notification.
  • Approvers are told. When a subject enters the approval funnel — a governed change or workflow run held pending_approval, a trust raise, a release promotion or a transport grant — every user the approvals inbox would show it waiting on you to (named on a line, or a member of a group a line names, with that line still unsatisfied; the requester never) receives a realtime in-app notification linking to the inbox, authorised by the inbox's own rule (read on the checklist target; a release promotion or a lane rollover — whose target is the platform root or the agent's path — reaches the approvers it names regardless). The ceremony's closure lands in the same subject thread, so the pending call to action is superseded instead of left standing. The GUI's Inbox sidebar entry carries the matching count in its red bubble.
  • An engine that fails several consecutive sweeps says so loudly: warn:notifications:engine — dispatch is stalled.

Who must hear: subscriptions and mandates​

  • Personal subscriptions are each user's own declared interests — scope (estate node-path selector or the platform domain) × conditions (object kinds, event kinds, state predicates, severity floor, attribute and metadata predicates) × route × controls (enabled, mute, snooze, cadence). Strictly caller-scoped: no caller, superuser included, reaches another user's subscriptions, routes or feed through the user plane.
  • Team subscriptions are group-owned: the same selector grammar, matched against the group's own path-RBAC grants (never a union of members' personal grants) and delivered once per event through a group-owned, connector-backed route — a team channel, not a per-member fan-out (an optional in-app mirror row per member exists, off by default). Members see them read-only; managing them takes the group's admin determination.
  • Mandates are the governance shape: a subscription targeted at one group whose members cannot delete, mute, snooze or disable it — the member verbs refuse a mandate's id with the typed MANDATED_SUBSCRIPTION reason. A mandate names a group, and the group is whichever population carries it: membership resolves through the provider-group snapshots folded with the local groups (the same union the approvals inbox and the member-facing verbs resolve), so a directory move or a local-group edit takes effect without touching the mandate, and an identity both populations carry is delivered to once; delivery is in-app and realtime, and the matching mandate's id is stamped onto the dispatched row as the evidence attribution. Deleting a mandate keeps its dispatched evidence rows (their attribution clears). The hands-on guide: opschain learn governance-notifications.

How it reaches you: connectors and routes​

A connector is an instance-level admin object — the configured egress a route may point at, never a user-pasted webhook URL. Three kinds:

  • webhook — the generic kind: an admin-allowlisted set of HTTPS URL prefixes, an HMAC-SHA256 signing secret (write-only: encrypted at rest, never echoed — reads serve signing_secret_set), and optional non-secret default headers. Deliveries are signed over a timestamped body (X-OpsChain-Signature: v1=… with X-OpsChain-Timestamp — the same idiom the inbound lane verifies, see inbound-webhooks), and each route picks a target URL inside the allowlist plus a payload template (generic, slack or teams incoming-webhook shapes).
  • slack and teams — the native chat kinds: the connector itself stores the incoming-webhook URL — a capability secret, held in the same encrypted write-only slot (webhook_url_set) — and the kind implies the payload shape, so these carry no allowlist, signing secret or template.

The HTTP posture is deliberately hostile to SSRF: HTTPS only, redirects never followed, bounded timeouts, and targets resolving to loopback, link-local (the cloud metadata range included), unspecified or multicast addresses are refused unless the admin allowlisted that address literally; resolution is pinned so a rebinding resolver cannot swap the target between check and dial.

A route is what a subscription points at: in_app (the always-on default), or a connector-backed kind bound to a connector of the same kind. Any authenticated caller may list the enabled connectors' id/name/kind catalogue (the route picker); everything else on the connector surface is admin-plane. The test verb sends one signed test payload (to a URL inside a webhook connector's allowlist, or a chat connector's stored URL) and reports the outcome without failing the call. Deleting a connector a route still points at is refused by name.

Connector-routed deliveries are swept cluster-wide under a database-leased claim: up to 5 attempts on a backoff ladder (30s/2m/8m/30m), then the row dead-letters as failed with the reason in delivery_error — the in-app feed row stays readable whatever its egress fate. An unsigned webhook delivery is never sent.

Delivery evidence​

Every dispatched row doubles as the receipt: who was told what, when, on which channel, with what outcome — queued, delivered, failed or suppressed (a dispatch-time authorisation re-check refusal) — metadata only, never rendered secret content. The admin query pages it newest first with composable filters (username, channel_kind, delivery_status, subscription_id, mandate_id, from/to): opschain admin notifications deliveries (opschain docs cli/admin-notifications-deliveries). A failed delivery row under a mandate is a finding — the mandate said this person must know, and the channel did not deliver. Team deliveries claim evidence in the group's name (the recipient is the group channel, not a member).

Retention​

Two clocks on the one record, enforced by an hourly set-based reaper on every replica:

  • Feed retention — age at which a row is archived out of every feed, thread and unread view (it survives as evidence).
  • Evidence retention — age at which the row is deleted outright; always at least the feed value.

The policy derives from the licence tier: Basic is fixed at 30 days feed / 90 days evidence; Enterprise defaults to 90 / 400 and is tunable (1–3650 days, evidence ≥ feed enforced at write) through opschain admin notifications retention show | set (opschain docs cli/admin-notifications-retention) — stored as the global settings keys notifications.feed_retention_days and notifications.evidence_retention_days (opschain docs settings-reference). The reaper fails safe towards retention: a licence or settings resolution failure skips the sweep rather than falling back to a shorter policy, and a sweep that removed something reports itself as a notifications:retention:sweep event, never log-only.

Entitlement posture — what is base, what is the pack​

The engine's matching, the zero-config defaults, personal and team subscriptions, routes, connectors, the delivery sweeper, enforcement of existing mandates (the member-verb refusals included) and the retention reaper are base platform — they never consult the licence. The admin governance surfaces — mandate administration, the delivery-evidence query and the retention configuration — gate on packs:security-governance (the typed refusal names the pack; see the security-governance pack and entitlements): a lapsed entitlement freezes administration without dropping the org's guaranteed coverage — the controls stay ungated, the admin surfaces are the pack. Retention tunability additionally reads the tier, as above.

Governance and audit​

Member and admin writes land as ordinary audit events (opschain docs events):

  • api:notification_subscriptions:create | update | destroy | mute | snooze and api:notification_routes:create | update | destroy — the user plane.
  • api:notification_team_subscriptions:* and api:notification_team_routes:* — the group-owned surfaces.
  • api:notifications:read / api:notifications:archive — feed rows stamped (counts only, never summaries).
  • api:notification_connectors:create | update | destroy | test — the connector surface (names and ids only — never allowlists, headers or secret material; a test also records its outcome).
  • api:notification_mandates:create | update | destroy and api:notification_retention:update — the governance arm.
  • warn:notifications:engine and notifications:retention:sweep — the engine's own health and the reaper's receipts.
  • opschain docs cli/admin-notifications — the admin CLI family: connectors (create | update | delete | test | list), mandates (create | update | delete | list), deliveries, retention (show | set).
  • opschain docs resources/notification / opschain docs resources/notification-connector — the data dictionaries, selector grammar and noise-control mechanics in full.
  • opschain learn governance-notifications — mandates, evidence and retention, hands-on.
  • the security-governance pack and entitlements — the packs:security-governance gate.
  • opschain docs api-v2 — NotificationService and NotificationAdminService on the v2 surface.