notification-service
Bounded Context: Communication (Generic) · Owner: Platform Services squad · Phase: 0 (email + in-app + SMS S0; WhatsApp S1; Voice/IVR Phase 3+) · Storage: Cloud SQL Postgres (shared schema + RLS) + transactional outbox · Bundle: services/notification-service/
notification-service is the single mouth of Ghasi Melmastoon — the multi-tenant hotel SaaS platform whose backoffice is an Electron offline-first desktop app and whose cloud is GCP. No other service directly emails, SMSes, WhatsApps, pushes, calls, or in-app-pings a recipient. Every outbound communication — guest confirmations, dunning, mobile-key delivery, password resets, vendor work-order alerts, marketing blasts — flows through this service so that preferences, branding, suppression, audit, rate limits, and per-market sender-ID compliance are enforced uniformly.
The service does not decide what to say. It consumes domain events from sibling services (reservation-service, billing-service, lock-integration-service, iam-service, tenant-service, maintenance-service), maps them through the trigger map to template keys, renders templates with per-tenant branding in the recipient's locale (Pashto, Dari, Arabic RTL; English, French LTR), gates against per-recipient preferences and the suppression list, and dispatches over the appropriate channel adapter via the NotificationPort abstraction. AI-drafted personalised copy and translations are produced by ai-orchestrator-service and arrive here as already-rendered content with an AIProvenance block; we never call models directly.
Purpose
- Be the single platform-wide outbound channel for guest, staff, and tenant-admin communication. No service writes to SMTP, Twilio, FCM, APNs, WhatsApp Business API, or a voice gateway except us.
- Enforce preferences, opt-outs, suppression, quiet hours, per-market sender-ID rules, and per-recipient/per-tenant rate limits uniformly.
- Render multi-language, RTL/LTR, per-tenant-branded templates with the variables the upstream event carried. The Reservation event says "this booking confirmed"; we say "Mr. Karimi, تأییدیهٔ رزرو شما در هتل گهسی ملمستون…" with the tenant's logo, in Dari, with the right SMS sender-ID for
+93numbers. - Capture the delivery audit for every outbound message (queued → dispatched → delivered → opened → clicked, or failed/bounced/suppressed) for 7-year regulated retention.
- Ingest vendor delivery webhooks (SendGrid events, Twilio status callbacks, FCM/APNs feedback, WhatsApp Business API status, Stripe-style ESP webhooks) and feed them back into the audit + suppression list.
- Provide a template authoring surface (preview, test-send, version + publish workflow) that staff content authors and tenant admins can use without engineer involvement.
Key responsibilities
- Event-triggered notifications — subscribe to platform domain events; map each event to one or more template keys via the trigger map; enqueue per-recipient
Notificationrows; dispatch over channel(s) the recipient prefers. - Ad-hoc notifications API for staff (re-send confirmation, send invoice copy, marketing blast, manual SMS to a guest from the front-desk console).
- Template management — versioned, multi-language, RTL/LTR templates; per-tenant overrides on top of platform-global defaults; preview + test-send + publish workflow with HITL approval for AI-drafted content.
- Per-recipient preferences — channels, languages, opt-out per category (transactional / operational / marketing); regulatory overrides for security and compliance categories that cannot be opted out.
- Per-tenant branding & sender identity — from-name, from-email (DKIM/SPF/DMARC verified per tenant domain), SMS sender-ID (PK/UAE require registered alphanumeric sender-IDs; AF/IR fall back to long-code shortcodes), WhatsApp Business display name, voice caller ID.
- Delivery audit — every
Notificationcarries its lifecycle attempts, vendor message id, vendor response, latency, bounce/complaint reason. Retained 7 years for regulated categories (financial / lock-credential / KYC) and 1 year for operational. - Webhook ingestion for vendor delivery acks; HMAC-validated; updates the
Notificationstate and feeds the suppression list (hard bounces, complaints). - Bounce/complaint handling with auto-suppression — hard bounces and explicit complaints add the recipient address to the per-tenant suppression list; admin can override with audit.
- Rate limiting — per-tenant per-channel per-day; per-recipient per-day; backoff on vendor 429/5xx; circuit breaker per (tenant, channel) when a vendor degrades.
- Channel fallback — if WhatsApp template approval is pending or the vendor is unreachable, fall back to SMS for high-priority categories (booking confirmation); escalate to email if SMS also fails.
- Scheduled sends — booking-confirmation immediate, pre-arrival reminder T-24h, post-stay thank-you T+24h, dunning sequence (T+0, T+3, T+7), all driven by a scheduler worker.
- Mobile-key delivery — coordinate with
lock-integration-serviceto deliver one-time mobile-key links/QR codes via the recipient's chosen channel with single-use tokens.
Hotel-specific shape
- SMS is the most reliable channel in target markets (Afghanistan, Tajikistan, Iran rural, Pakistan provinces). Many guests are on feature phones; SMS reaches them when nothing else does.
- WhatsApp is dominant for guest comms in AF/PK/IR. We integrate the WhatsApp Business API with pre-approved template messages (
utility,marketing,authenticationcategories) — Meta requires template approval before sending; pending approval blocks dispatch and alerts the tenant admin. - Email is for receipts and invoices — less reliable in some markets but legally required as proof of payment for many tenants.
- In-app push is for the consumer mobile booking app (FCM for Android, APNs for iOS, Web Push for browser).
- Voice/IVR (Phase 3+) — confirmation calls in the guest's language for high-value bookings; uses Twilio Voice with TTS in Pashto/Dari/Arabic.
- Sender-ID localization — Pakistan PTA requires registered alphanumeric sender-IDs (e.g.,
GHASI); UAE TRA same; AF/IR/TJ accept generic long codes. We maintain a per-(tenant, country) sender-ID registry and the dispatcher selects the right one per recipient phone country code. - RTL templates — Pashto, Dari, Arabic templates are stored with
dir="rtl"and use locale-aware date/number/currency formatting. The MJML/HTML renderer flips margins and text alignment automatically. - Content-rich templates — booking confirmation embeds dates, room type, nightly breakdown, total, balance due (cash on arrival is common), tenant branding header, property address, contact phone, cancellation policy summary, in the guest's language.
Aggregates owned
| Aggregate | Cardinality | Purpose | Identity prefix |
|---|---|---|---|
Notification | 1 per (recipient, channel, send-intent) | The dispatch record: status, attempts, render snapshot, suppression reason | ntf_ |
Template | platform-global or tenant-overridden | Logical template by key; carries the active version pointer | tpl_ |
TemplateVersion | 1..N per Template | Immutable rendered body per locale; semver-versioned; draft → active → archived | tpv_ |
Recipient | 1 per (tenant, contact identity) | Resolved contact identity (guest, staff, tenant-admin, vendor) with verified addresses | rcp_ |
RecipientPreferences | 1 per Recipient | Channel, locale, opt-outs per category; quiet-hours; guardian routing where minors apply | (composite) |
DeliveryAttempt | 1..N per Notification | Per-attempt provider record (vendor name, vendor message id, latency, outcome) | (composite, ULID) |
SuppressionRecord | 1 per (tenant, channel, address-hash) | Hard-bounce / complaint / manual block; one row per suppressed address | sup_ |
Channel | 1 per (tenant, channel-kind) | Tenant-level channel configuration (sender identity, vendor selection, status) | ch_ |
ChannelCredential | 1..N per Channel | Vendor credentials (API key ciphertext, sender-ID, DKIM selector, WhatsApp display name) | chc_ |
WebhookInbound | 1 per inbound vendor callback | Audit row of every vendor delivery webhook received and processed | whi_ |
Key APIs (REST, /api/v1/notifications, /api/v1/notification-templates, /api/v1/notification-preferences, /api/v1/notification-channels)
| Method | Path | Purpose |
|---|---|---|
POST | /api/v1/notifications | Internal: enqueue an ad-hoc notification (staff-initiated or service-initiated) |
POST | /api/v1/notifications/batch | Internal: enqueue a batch (marketing blast) |
GET | /api/v1/notifications/:id | Read one notification (audit) |
GET | /api/v1/notifications | List + filter (by recipient, by source-event, by status) |
POST | /api/v1/notifications/:id/resend | Re-send (creates a new sibling notification with same template/vars) |
GET | /api/v1/notifications/:id/audit | Full delivery-attempt + webhook trail |
GET | /api/v1/notification-templates | List templates (platform + tenant) |
POST | /api/v1/notification-templates | Create template / new version (draft) |
POST | /api/v1/notification-templates/:id/preview | Render preview with sample variables |
POST | /api/v1/notification-templates/:id/test-send | Send to caller's verified address |
POST | /api/v1/notification-templates/:id/publish | Activate (with HITL approval if AI-drafted) |
POST | /api/v1/notification-templates/:id/archive | Archive (with successor key) |
GET | /api/v1/notification-preferences/:recipientId | Read preferences |
PATCH | /api/v1/notification-preferences/:recipientId | Update preferences (RFC-7396 merge-patch) |
POST | /api/v1/notification-preferences/opt-out/:token | Public unsubscribe via signed token (no auth) |
GET | /api/v1/notification-channels | List per-tenant channel configurations |
PATCH | /api/v1/notification-channels/:id | Update channel config (sender-ID, vendor, status) |
POST | /api/v1/notification-channels/:id/health-check | Probe vendor health and report |
GET | /api/v1/suppressions | List suppressed addresses for the tenant |
POST | /api/v1/suppressions/:id/release | Manual unblock (audited) |
POST | /api/v1/webhooks/vendors/:vendor | Inbound vendor delivery webhook (HMAC-validated) |
Consumed by bff-backoffice-service (template authoring, audit search, manual sends), bff-tenant-booking-service (unsubscribe surface), and internally by every other service via the NotificationClient SDK.
Key events published
| Event | Trigger |
|---|---|
melmastoon.notification.requested.v1 | Internal/external request accepted (pre-render) |
melmastoon.notification.scheduled.v1 | Send scheduled for a future time (pre-arrival reminder, dunning) |
melmastoon.notification.dispatched.v1 | Handed off to vendor adapter |
melmastoon.notification.delivered.v1 | Vendor confirmed delivery |
melmastoon.notification.failed.v1 | Terminal failure after retries |
melmastoon.notification.bounced.v1 | Vendor bounce (hard/soft) |
melmastoon.notification.opened.v1 | Email open pixel fired |
melmastoon.notification.clicked.v1 | Tracked link clicked |
melmastoon.notification.opted_out.v1 | Recipient unsubscribed |
melmastoon.notification.suppressed.v1 | Send blocked by preferences/suppression list |
melmastoon.notification.template.published.v1 | Template version activated |
melmastoon.notification.template.archived.v1 | Template version archived |
melmastoon.notification.preferences.updated.v1 | Recipient preferences changed |
melmastoon.notification.channel.health_changed.v1 | Vendor health flipped (healthy ↔ degraded ↔ down) |
Key events consumed
| Event | Effect |
|---|---|
melmastoon.reservation.confirmed.v1 | Send booking confirmation (email + SMS or WhatsApp) |
melmastoon.reservation.cancelled.v1 | Send cancellation notice with refund detail (if any) |
melmastoon.reservation.modified.v1 | Send modification confirmation (delta summary) |
melmastoon.reservation.dates_changed.v1 | Send date-change confirmation; re-issue mobile-key link if already issued |
melmastoon.reservation.checked_in.v1 | Send welcome/Wi-Fi message; deliver mobile-key (with lock_integration data) |
melmastoon.reservation.checked_out.v1 | Send thank-you + invoice copy + review request |
melmastoon.reservation.confirmed.v1 (T-24h scheduled) | Pre-arrival reminder with directions + check-in window |
melmastoon.lock_integration.key_credential.issued.v1 | Deliver mobile-key link/QR over guest's chosen channel |
melmastoon.lock_integration.key_credential.revoked.v1 | Notify guest if revoked early-checkout-induced |
melmastoon.billing.invoice.generated.v1 | Send invoice attachment to guest |
melmastoon.billing.subscription.payment_failed.v1 | Tenant-admin dunning sequence (T+0, T+3, T+7) |
melmastoon.iam.password.reset_requested.v1 | Email password-reset link (regulated, opt-out blocked) |
melmastoon.tenant.invitation.sent.v1 | Email invitation link to invited user |
melmastoon.maintenance.work_order.assigned.v1 | SMS the assigned vendor with location + access detail |
melmastoon.tenant.settings.changed.v1 | Refresh per-tenant channel config and sender-ID cache |
Upstream / downstream
Upstream (we consume): reservation-service, billing-service, lock-integration-service, iam-service, tenant-service, maintenance-service, ai-orchestrator-service (for AI-drafted content delivered via ai_drafted_content_ready.v1).
Downstream (we publish for): analytics-service, audit-service, bff-backoffice-service (for admin notification audit views and unread badges).
Non-functional requirements
| NFR | Target |
|---|---|
| Email enqueue → vendor-accepted p95 | < 60 s |
| SMS enqueue → vendor-accepted p95 | < 30 s |
| WhatsApp enqueue → vendor-accepted p95 | < 30 s |
| Push enqueue → device p95 | < 10 s |
| In-app delivery p95 | < 2 s |
| Vendor webhook ingest p95 | < 5 s |
| API availability | 99.95% monthly |
| Tenant isolation | RLS-enforced; tenant-isolation.spec.ts mandatory in CI |
| Bounce/complaint → suppression latency | < 5 min from webhook |
| Per-tenant per-channel daily budget | tenant-configurable; default 50K/day per channel; soft alert at 80% |
| Replicas | Min 3 Cloud Run instances (API), 2 dispatcher workers per channel, 2 scheduler workers, 1 webhook ingest |
Where to go next
- Implementation-grade detail:
services/notification-service/SERVICE_OVERVIEW.mdand the rest of the 17-doc bundle. - Booking-saga notifications and consumption ordering:
docs/04-event-driven-architecture.md§7. - API conventions and Problem+JSON envelope:
docs/05-api-design.md. - Per-tenant branding and theme tokens used by templates:
docs/03-microservices/theme-config-service.md. - AI-drafted content path and HITL:
docs/08-ai-architecture.mdandservices/ai-orchestrator-service/AI_INTEGRATION.md. - Mobile-key delivery interaction:
docs/09-lock-and-key-integration.md.