Notification Service
EventBridge-triggered service that dispatches notifications through pluggable channels (SES, Google Chat) and logs every delivery attempt to DynamoDB.
Service Overview
From its README: an “EventBridge-triggered Lambda service that dispatches notifications through pluggable channels (SES, GOOGLE_CHAT, etc.) and logs every delivery attempt to DynamoDB.”
It is really three deployables in one repository, and this matters because each publishes on a
different source:
| Component | Path | source it publishes on |
|---|---|---|
| FastAPI notification-centre API (ECS) | api/ | rio.notification.api |
| Forecast reminder Lambda | lambda/src/forecast_notification/ | rio.forecast-notification |
| Dispatch Lambda | lambda/src/notification_service/ | rio.platform.notification |
It talks to itself
The forecast Lambda publishes Create Notification, and the dispatch Lambda subscribes to it. So the
service is its own biggest customer:
forecast Lambda --(Create Notification)--> dispatch Lambda --> SES / Google ChatThe rule that does this is CreateNotificationRule (infrastructure/template.yaml:398-405). Note
that it filters on detail-type only — there is no source filter — so any service on the bus
can emit Create Notification and have it delivered. That is the intended integration point for
other teams.
What it listens for
| Rule | Matches | Notes |
|---|---|---|
CreateNotificationRule (template.yaml:398-405) | detail-type = Create Notification | No source filter — open to any publisher |
NotificationRule (template.yaml:406-416) | detail.target_service = rio.platform.notification and detail.event_name in rio.core.activity.emailingestionfailed, rio.core.activity.emailsyncfailed | Matches on detail fields only — no source, no detail-type |
The second rule is unusual: it routes on payload contents rather than on the envelope. The
Activity Signal Service cooperates by stamping target_service: "rio.platform.notification" into the
detail of its failure events specifically so this rule will pick them up.
The runtime guard mirrors the rule (lambda/src/notification_service/service.py:28-36) with
SUPPORTED_EVENT_NAMES and SUPPORTED_DETAIL_TYPES — so both the infrastructure and the code have
to be updated together when adding a new trigger.
Scheduled reminders
The forecast Lambda is driven by three AWS::Scheduler schedules, not by events
(infrastructure/template.yaml:467-495):
| Schedule | Cron | What it nudges |
|---|---|---|
${DeployPrefix}-user-reminder | cron(0 12 ? * THU *) | Reps who have not submitted |
${DeployPrefix}-manager-reminder | cron(0 12 ? * FRI *) | Managers who have not reviewed |
${DeployPrefix}-manager-adjustment-reminder | cron(0 16 ? * FRI *) | Managers who have not adjusted |
HTTP API
| Method | Path | Purpose |
|---|---|---|
GET | /health | Health check, also verifies the DynamoDB inbox table |
GET | /tenants/{tenant_id}/alerts | List a user’s alerts, paginated |
PATCH | /tenants/{tenant_id}/alerts/{notification_id} | Mark read / unread — emits Notification Updated |
Both alert routes are proxied through rio-app-layer and consumed by rio-ui.
Data stores
DynamoDB only — no relational tables are owned here.
${DeployPrefix}-log(template.yaml:234-251) — one row per delivery attempt. Keynotification_id+created_at, with a TTL onttl.${DeployPrefix}-notification-inbox(template.yaml:256+) — backs the in-app notification centre. Keypk/sk, with three GSIs:entity-index,correlation-index,status-severity-index.
It also reads the RDS tables dim_person and fact_forecast to work out who to remind, but does
not own them.
Failure handling
Both Lambdas have their own dead-letter queue — ${DeployPrefix}-notification-dlq (:361) and
${DeployPrefix}-forecast-notification-dlq (:422). These are DLQs, not subscriptions; nothing
reads from them automatically.