Skip to main content

Notification Service — Application Logic

Status: populated Owner: Platform Engineering Last updated: 2026-04-18

1. Use Cases

Use caseTriggerNotes
SendUserRegisteredNotificationUseCaseauth.user.registered.v1Welcome email to new user
SendInvoiceGeneratedNotificationUseCasebilling.invoice.generated.v1Invoice ready email to account admin(s)
SendOperatorDownNotificationUseCaseoperator.health.status_changed.v1 where newStatus=DOWNAlert to platform admins; SMS + email
SendSystemAlertNotificationUseCasesystem.alerts.*Alert to platform admins; channel depends on severity
GetNotificationLogUseCaseInternal admin endpointRead notification_log with filtering
GetPreferencesUseCaseInternal admin endpointRead preferences for account
UpdatePreferencesUseCaseInternal admin endpointSet opt-out by category+channel
ListTemplatesUseCaseInternal admin endpointCRUD for admin-dashboard
CreateTemplateUseCaseInternal admin endpointValidates variablesSchema; stores template
UpdateTemplateUseCaseInternal admin endpointIncrements version; old version retained for log reference
PreviewTemplateUseCaseInternal admin endpointRenders template with sample variables; returns HTML for preview

2. Ports

PortAdapter
NotificationLogRepositoryPrisma (notif.notification_log)
PreferenceRepositoryPrisma (notif.notification_preferences)
TemplateRepositoryPrisma (notif.notification_templates)
EmailDeliveryPortSendGrid @sendgrid/mail SDK
SmsDeliveryPortHTTP POST to sms-orchestrator POST /v1/sms/send
RecipientResolverHTTP GET to auth-service /v1/users/{id} to resolve email/phone
EventConsumerNATS 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): userId in event → auth-service lookup.
  • For account events (billing.events): accountId in event → auth-service list account.admin users.
  • For platform events (operator.health, system.alerts): auth-service list platform.admin users (cached 5 min in Redis).
  • Recipient cache key: notif:recipients:platform_admins TTL 300s.