Notification Service — Application Logic
Status: populated Owner: Platform Engineering Last updated: 2026-04-18
1. Use Cases
| Use case | Trigger | Notes |
|---|---|---|
SendUserRegisteredNotificationUseCase | auth.user.registered.v1 | Welcome email to new user |
SendInvoiceGeneratedNotificationUseCase | billing.invoice.generated.v1 | Invoice ready email to account admin(s) |
SendOperatorDownNotificationUseCase | operator.health.status_changed.v1 where newStatus=DOWN | Alert to platform admins; SMS + email |
SendSystemAlertNotificationUseCase | system.alerts.* | Alert to platform admins; channel depends on severity |
GetNotificationLogUseCase | Internal admin endpoint | Read notification_log with filtering |
GetPreferencesUseCase | Internal admin endpoint | Read preferences for account |
UpdatePreferencesUseCase | Internal admin endpoint | Set opt-out by category+channel |
ListTemplatesUseCase | Internal admin endpoint | CRUD for admin-dashboard |
CreateTemplateUseCase | Internal admin endpoint | Validates variablesSchema; stores template |
UpdateTemplateUseCase | Internal admin endpoint | Increments version; old version retained for log reference |
PreviewTemplateUseCase | Internal admin endpoint | Renders template with sample variables; returns HTML for preview |
2. Ports
| Port | Adapter |
|---|---|
NotificationLogRepository | Prisma (notif.notification_log) |
PreferenceRepository | Prisma (notif.notification_preferences) |
TemplateRepository | Prisma (notif.notification_templates) |
EmailDeliveryPort | SendGrid @sendgrid/mail SDK |
SmsDeliveryPort | HTTP POST to sms-orchestrator POST /v1/sms/send |
RecipientResolver | HTTP GET to auth-service /v1/users/{id} to resolve email/phone |
EventConsumer | NATS JetStream multi-subject consumer |
3. NotificationDispatcher — Detailed Flow
1. Receive NATS event
2. Route by event type → NotificationUseCase
3. Use case: resolve recipient(s)
a. auth-service GET /v1/users/{userId} → { email, phone }
b. For platform alerts: fetch all platform.admin users from auth-service
4. For each recipient × channel:
a. PreferenceResolver.isOptedOut(accountId, category, channel)
- If opted out AND category not in [SYSTEM_SECURITY, ACCOUNT, BILLING] → INSERT log(SUPPRESSED); skip
b. TemplateRepository.findActive(type, channel)
c. TemplateRenderer.render(template, variables) → { subject, htmlBody, textBody }
d. EmailDeliveryPort.send() OR SmsDeliveryPort.send()
e. INSERT notification_log(status=SENT|FAILED, providerMessageId, attemptCount)
5. ACK NATS message
Retry policy (transient delivery failures):
- Retry up to 3 attempts with 5s, 30s, 2min backoff (in-process; not NATS redelivery).
- After 3 failures → status=FAILED; ACK NATS (do not loop indefinitely); alert fires.
- NATS NAK only on infrastructure errors (PG down, template missing) — these need ops intervention.
4. Template Rendering Pipeline
1. Load template.bodyHtml (Handlebars source with Mjml tags)
2. Validate variables against template.variablesSchema (JSON Schema)
3. Handlebars.compile(bodyHtml)(variables) → raw Mjml string
4. mjml(rawMjml) → { html, errors }
5. If errors → throw TemplateRenderError (logged; use bodyText fallback)
6. Use html for email, bodyText for SMS / plain-text fallback
5. SMS Delivery via sms-orchestrator
Notification SMS are sent as low-priority with metadata:
{
"to": "{recipientPhone}",
"from": "Ghasi",
"body": "{renderedTextBody}",
"messageType": "SMS",
"metadata": { "notificationId": "uuid", "category": "OPERATOR_ALERT", "priority": "low" }
}
Uses a dedicated API key with scope sms:send and low-priority routing metadata so sms-orchestrator can deprioritize notification SMS behind customer traffic.
6. Recipient Resolution
- For user events (
auth.events):userIdin event → auth-service lookup. - For account events (
billing.events):accountIdin event → auth-service listaccount.adminusers. - For platform events (
operator.health,system.alerts): auth-service listplatform.adminusers (cached 5 min in Redis). - Recipient cache key:
notif:recipients:platform_adminsTTL 300s.