Skip to main content
Version: v5.6.1

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 Reviews & approvals sidebar entry carries the matching count as a 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.