Notifier (Notifications Studio Host)

Notifier is the deployable host for the Stella Ops Notifications Studio — the WebService and Worker that turn platform events into delivered, audited notifications. This page is for operators running the notifier and for engineers integrating with its event-ingest and management APIs.

Status: Implemented Source: src/Notifier/ Owner: Notify Guild

Note: Notifier is the deployment host for the Notifications Studio. For the underlying notification toolkit (engine, storage, queue, connectors), see ../notify/.

Purpose

Notifier provides the deployable WebService and Worker that compose the Notify libraries into the Notifications Studio experience. It is the entry point for event ingestion, rule/channel/template management, delivery history, incident correlation and escalation, on-call schedules, quiet-hours and maintenance suppression, throttling and storm-breaking, localization, dead-letter administration, retention, and webhook security.

The WebService and Worker are independent ASP.NET Core hosts (both target net10.0) that share the Notify PostgreSQL notify schema and the Notify event queue.

Latest Updates

Operations: ack-bridge external-ID durability

The PagerDuty / OpsGenie acknowledgement webhooks resolve their inbound externalId (PagerDuty dedup_key, OpsGenie alias) against notify.deliveries to find the matching tenant + incident. The resolver has one durable path: indexed lookup against notify.deliveries.external_id joined to notify.channels on (channel_id, tenant_id). It survives Postgres restarts and connection-pool drops because the value lives in a real column with the partial composite index idx_notify_deliveries_external_id (channel_id, external_id) WHERE external_id IS NOT NULL (added in migration 009_deliveries_external_id_columns.sql).

There is no JSON metadata fallback in non-testing code. Rows that still carry legacy event_payload.metadata.externalId / metadata.channel are normalized by migration 009_deliveries_external_id_columns.sql and cleaned by 010_deliveries_drop_metadata_externalid_keys.sql. If a delivery is missing the first-class external_id, the ack bridge does not resolve it from JSON.

Relationship to Notify

ComponentPathPurpose
Notifysrc/Notify/Reusable toolkit: engine, models, connectors, queue
Notifiersrc/Notifier/Host: WebService and Worker that compose Notify

Per 2025-11-02 module boundary decision: Maintain separation for packaging, offline kit parity, and cross-module governance.

Components

Deployables:

Integration Points:

Authorization scopes

The WebService validates the following scopes (defined canonically in StellaOps.Auth.Abstractions.StellaOpsScopes and mapped to named policies in WebService/Constants/NotifierPolicies.cs):

ScopePolicy constantPurpose
notify.viewerNotifyViewerRead-only access to channels, rules, templates, delivery history, observability. Default policy on the /api/v2/notify group.
notify.operatorNotifyOperatorRule/channel/template authoring, delivery actions, simulation, event ingest (POST /api/v1/events/{producer}).
notify.adminNotifyAdminSecurity configuration, signing-key rotation, tenant-isolation grants, retention policies, platform-wide settings.
notify.escalateNotifyEscalateEscalation actions and on-call/escalation-policy management.

Most tenant-data routes require a validated stellaops:tenant claim; the gateway may still forward X-StellaOps-TenantId as envelope context, but Notifier does not use raw tenant headers as the data-isolation source. Write endpoints read an optional X-StellaOps-Actor header for audit attribution.

API surface

The route table below is grounded in WebService/Program.cs and WebService/Endpoints/*.cs. Some /api/v2/notify/* routes in Program.cs and the corresponding /api/v2/* group endpoints (e.g. /api/v2/notify/quiet-hours vs /api/v2/quiet-hours/calendars) coexist as canonical + legacy-compat surfaces; see the Latest Updates entries.

Authorization caveat (verify before relying on it): the /api/v2/notify rules/templates/incidents endpoints (Endpoints/NotifyApiEndpoints.cs) and the other Endpoints/*.cs groups (escalation, fallback, incidents, localization, observability, security) attach .RequireAuthorization(...) per route. The inline /api/v2/notify/* routes defined directly in Program.cs (channels, deliveries, deliveries/retry+stats, simulate, quiet-hours, maintenance-windows, throttle-configs, overrides, escalation-policies, oncall-schedules, inbox, localization, storms, security, dead-letter, retention) do not call .RequireAuthorization and rely on tenant-claim context middleware only — they are not scope-gated at the route level. Only POST /api/v1/events/{producer} among the Program.cs routes requires a scope (notify.operator). The PagerDuty/OpsGenie ack webhooks under /api/v2/ack/webhook/* are AllowAnonymous.

Event ingest (v1):

Notify v2 (/api/v2/notify): Rules/templates/incidents are mapped by MapNotifyApiV2 with a notify.viewer group default and notify.operator on writes. The remaining routes in this section are mapped inline in Program.cs and are not scope-gated (see the authorization caveat above).

Other v2 route groups (registered from Endpoints/*.cs): /api/v2/rules, /api/v2/templates, /api/v2/incidents (+ WebSocket live feed via MapIncidentLiveFeed), /api/v2/simulate, /api/v2/quiet-hours, /api/v2/throttles, /api/v2/overrides, /api/v2/escalation-policies, /api/v2/oncall-schedules, /api/v2/escalations, /api/v2/ack (incl. /webhook/pagerduty, /webhook/opsgenie), /api/v2/storm-breaker, /api/v2/localization, /api/v2/fallback, /api/v2/security, and /api/v1/observability (metrics, dead-letters, chaos experiments, retention policies).

Operational: GET /healthz (health check), GET /.well-known/openapi (OpenAPI document). Requests under /api/v1 emit RFC 8594 Deprecation/Sunset headers.