service

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:

ComponentPathsource it publishes on
FastAPI notification-centre API (ECS)api/rio.notification.api
Forecast reminder Lambdalambda/src/forecast_notification/rio.forecast-notification
Dispatch Lambdalambda/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 Chat

The 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

RuleMatchesNotes
CreateNotificationRule (template.yaml:398-405)detail-type = Create NotificationNo 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.emailsyncfailedMatches 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):

ScheduleCronWhat it nudges
${DeployPrefix}-user-remindercron(0 12 ? * THU *)Reps who have not submitted
${DeployPrefix}-manager-remindercron(0 12 ? * FRI *)Managers who have not reviewed
${DeployPrefix}-manager-adjustment-remindercron(0 16 ? * FRI *)Managers who have not adjusted

HTTP API

MethodPathPurpose
GET/healthHealth check, also verifies the DynamoDB inbox table
GET/tenants/{tenant_id}/alertsList 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. Key notification_id + created_at, with a TTL on ttl.
  • ${DeployPrefix}-notification-inbox (template.yaml:256+) — backs the in-app notification centre. Key pk/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.

Inbound and Outbound Message Flow

Event-driven architecture documentation: RIO