Skip to main content

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 +93 numbers.
  • 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

  1. Event-triggered notifications — subscribe to platform domain events; map each event to one or more template keys via the trigger map; enqueue per-recipient Notification rows; dispatch over channel(s) the recipient prefers.
  2. Ad-hoc notifications API for staff (re-send confirmation, send invoice copy, marketing blast, manual SMS to a guest from the front-desk console).
  3. 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.
  4. Per-recipient preferences — channels, languages, opt-out per category (transactional / operational / marketing); regulatory overrides for security and compliance categories that cannot be opted out.
  5. 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.
  6. Delivery audit — every Notification carries 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.
  7. Webhook ingestion for vendor delivery acks; HMAC-validated; updates the Notification state and feeds the suppression list (hard bounces, complaints).
  8. 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.
  9. 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.
  10. 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.
  11. 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.
  12. Mobile-key delivery — coordinate with lock-integration-service to 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, authentication categories) — 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

AggregateCardinalityPurposeIdentity prefix
Notification1 per (recipient, channel, send-intent)The dispatch record: status, attempts, render snapshot, suppression reasonntf_
Templateplatform-global or tenant-overriddenLogical template by key; carries the active version pointertpl_
TemplateVersion1..N per TemplateImmutable rendered body per locale; semver-versioned; draft → active → archivedtpv_
Recipient1 per (tenant, contact identity)Resolved contact identity (guest, staff, tenant-admin, vendor) with verified addressesrcp_
RecipientPreferences1 per RecipientChannel, locale, opt-outs per category; quiet-hours; guardian routing where minors apply(composite)
DeliveryAttempt1..N per NotificationPer-attempt provider record (vendor name, vendor message id, latency, outcome)(composite, ULID)
SuppressionRecord1 per (tenant, channel, address-hash)Hard-bounce / complaint / manual block; one row per suppressed addresssup_
Channel1 per (tenant, channel-kind)Tenant-level channel configuration (sender identity, vendor selection, status)ch_
ChannelCredential1..N per ChannelVendor credentials (API key ciphertext, sender-ID, DKIM selector, WhatsApp display name)chc_
WebhookInbound1 per inbound vendor callbackAudit row of every vendor delivery webhook received and processedwhi_

Key APIs (REST, /api/v1/notifications, /api/v1/notification-templates, /api/v1/notification-preferences, /api/v1/notification-channels)

MethodPathPurpose
POST/api/v1/notificationsInternal: enqueue an ad-hoc notification (staff-initiated or service-initiated)
POST/api/v1/notifications/batchInternal: enqueue a batch (marketing blast)
GET/api/v1/notifications/:idRead one notification (audit)
GET/api/v1/notificationsList + filter (by recipient, by source-event, by status)
POST/api/v1/notifications/:id/resendRe-send (creates a new sibling notification with same template/vars)
GET/api/v1/notifications/:id/auditFull delivery-attempt + webhook trail
GET/api/v1/notification-templatesList templates (platform + tenant)
POST/api/v1/notification-templatesCreate template / new version (draft)
POST/api/v1/notification-templates/:id/previewRender preview with sample variables
POST/api/v1/notification-templates/:id/test-sendSend to caller's verified address
POST/api/v1/notification-templates/:id/publishActivate (with HITL approval if AI-drafted)
POST/api/v1/notification-templates/:id/archiveArchive (with successor key)
GET/api/v1/notification-preferences/:recipientIdRead preferences
PATCH/api/v1/notification-preferences/:recipientIdUpdate preferences (RFC-7396 merge-patch)
POST/api/v1/notification-preferences/opt-out/:tokenPublic unsubscribe via signed token (no auth)
GET/api/v1/notification-channelsList per-tenant channel configurations
PATCH/api/v1/notification-channels/:idUpdate channel config (sender-ID, vendor, status)
POST/api/v1/notification-channels/:id/health-checkProbe vendor health and report
GET/api/v1/suppressionsList suppressed addresses for the tenant
POST/api/v1/suppressions/:id/releaseManual unblock (audited)
POST/api/v1/webhooks/vendors/:vendorInbound 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

EventTrigger
melmastoon.notification.requested.v1Internal/external request accepted (pre-render)
melmastoon.notification.scheduled.v1Send scheduled for a future time (pre-arrival reminder, dunning)
melmastoon.notification.dispatched.v1Handed off to vendor adapter
melmastoon.notification.delivered.v1Vendor confirmed delivery
melmastoon.notification.failed.v1Terminal failure after retries
melmastoon.notification.bounced.v1Vendor bounce (hard/soft)
melmastoon.notification.opened.v1Email open pixel fired
melmastoon.notification.clicked.v1Tracked link clicked
melmastoon.notification.opted_out.v1Recipient unsubscribed
melmastoon.notification.suppressed.v1Send blocked by preferences/suppression list
melmastoon.notification.template.published.v1Template version activated
melmastoon.notification.template.archived.v1Template version archived
melmastoon.notification.preferences.updated.v1Recipient preferences changed
melmastoon.notification.channel.health_changed.v1Vendor health flipped (healthy ↔ degraded ↔ down)

Key events consumed

EventEffect
melmastoon.reservation.confirmed.v1Send booking confirmation (email + SMS or WhatsApp)
melmastoon.reservation.cancelled.v1Send cancellation notice with refund detail (if any)
melmastoon.reservation.modified.v1Send modification confirmation (delta summary)
melmastoon.reservation.dates_changed.v1Send date-change confirmation; re-issue mobile-key link if already issued
melmastoon.reservation.checked_in.v1Send welcome/Wi-Fi message; deliver mobile-key (with lock_integration data)
melmastoon.reservation.checked_out.v1Send 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.v1Deliver mobile-key link/QR over guest's chosen channel
melmastoon.lock_integration.key_credential.revoked.v1Notify guest if revoked early-checkout-induced
melmastoon.billing.invoice.generated.v1Send invoice attachment to guest
melmastoon.billing.subscription.payment_failed.v1Tenant-admin dunning sequence (T+0, T+3, T+7)
melmastoon.iam.password.reset_requested.v1Email password-reset link (regulated, opt-out blocked)
melmastoon.tenant.invitation.sent.v1Email invitation link to invited user
melmastoon.maintenance.work_order.assigned.v1SMS the assigned vendor with location + access detail
melmastoon.tenant.settings.changed.v1Refresh 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

NFRTarget
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 availability99.95% monthly
Tenant isolationRLS-enforced; tenant-isolation.spec.ts mandatory in CI
Bounce/complaint → suppression latency< 5 min from webhook
Per-tenant per-channel daily budgettenant-configurable; default 50K/day per channel; soft alert at 80%
ReplicasMin 3 Cloud Run instances (API), 2 dispatcher workers per channel, 2 scheduler workers, 1 webhook ingest

Where to go next