Notification Pipeline
PSAP dispatcher creates Incident
POST /api/v1/incidents { agency_ids: [...], priority, incident_type, ... }
OR
InboundEventController receives signed event from PSAPLink PSAP instance
POST /api/v1/core/events/incident { incident_id, priority, company_agency_ids, ... }
|
v
NotificationService::dispatch(incident, agencyIds[])
|
├── For each company agency:
| └── Load all active users in that agency
| └── For each user:
| └── Load active UserTransportPreferences (ordered by priority_order)
| ├── Create Notification (user + incident) [ack_status = pending]
| ├── Create NotificationDelivery per transport preference
| └── Dispatch DeliveryMessage to Messenger bus (one per delivery)
|
v (async — Messenger worker)
TransportHandler::send(delivery)
├── Decrypt channel credentials from transport_channels.config
├── Call external provider (Twilio, Slack, etc.)
├── Update delivery.send_status (sent / failed)
├── CommunicationLogger::log(type: notification_sent)
└── Messenger::dispatch(EscalationMessage, [DelayStamp(ack_timeout_seconds)])
Two-Layer Model
Every outbound alert creates two database records:
Notification— one row per (incident, user). Tracks overall ACK status for that user.NotificationDelivery— one row per (notification, transport preference). Tracks delivery status for each channel.
This separation means a single user can be notified via SMS, email, and Slack simultaneously. One ACK from any channel closes all deliveries for that notification.
Delivery Statuses
| Status | Meaning |
|---|---|
pending | Created, not yet attempted |
queued | Handed to Messenger worker |
sent | Successfully handed to transport provider |
delivered | Provider confirmed delivery (where supported) |
failed | Transport returned an error |
Three Sole-Path Rules
These are non-negotiable architectural constraints:
-
CommunicationLoggeris the only path toincident_communications. Never write communication records directly. -
AckIngestionServiceis the only path to changingnotification.ack_status. All ACK sources (web, SMS reply, webhook) must go through this service. -
ACK cross-stream rule. When any delivery stream is ACKed,
AckIngestionServicemarks ALL deliveries for that notification as acknowledged and logs a single event naming the triggering stream and total count. Partial acknowledgement does not exist.