Notification Engine Module Technical Specifications

24 built-in HTML transactional email templates, a unified send API across every channel, push and webhook delivery, per-user preferences, event-driven routing, quiet hours, and digest bundling — across 8 delivery providers.

System Metrics

Delivery providers

8 (SendGrid, Mailgun, Postmark, AWS SES, generic SMTP, Twilio, FCM, APNs)

Data tracked

notifications, per-user preferences, push subscriptions, delivery logs, and custom templates — all exposed via the standard API and Admin UI

Built-in email handlers

24 transactional events

Notification channels

5 (email, SMS, push, in-app, webhook)

Push providers

FCM (Android), APNs (iOS), Web-Push (browser)

SMS providers

Twilio

Webhook retry

5 attempts, exponential backoff (5s, 10s, 20s, 40s — ~75s total budget)

Email delivery

SendGrid, Mailgun, Postmark, SES, SMTP (provider-agnostic)

Architecture Overview

CapabilityWhat it handles
**Unified API**Single send entry point with preference lookup, queueing, retry
**Email handlers**24 transactional email handlers + HTML email engine
**REST API**send / preferences / templates / logs / unsubscribe
**Preferences**Per-user channel opt-in/out, quiet hours, digest bundling
**Routing engine**Core notification routing decision logic
**Async queue**Background queue for non-blocking, retryable delivery
**Email webhooks**Bounce / delivery / open / spam-complaint tracking
**Provider adapters**In-app, email (SendGrid / Mailgun / etc.), push (Web push + FCM + APNs), SMS (Twilio), webhook
**Event listener**Inbound webhook processing

Unified Send API

What you send:

A single request shape covers every channel — target user, one or more delivery methods to attempt, an optional template name, optional subject/body overrides, arbitrary template data, a priority level, and an optional scheduled-for time.

What you get back:

A result that reports overall success, a notification ID for tracking, a per-channel delivery breakdown (method, success, provider message ID, provider name), and a list of any channels that were skipped along with the reason (e.g., user opted out, in quiet hours). Per-channel delivered/opened/clicked/bounced timestamps live in the delivery log, queried separately from the send response.

Send flow:

  1. A unique ID is generated for the notification.
  2. If a future send time is set, it's queued for delayed delivery.
  3. Otherwise it's enqueued for async, non-blocking delivery (or sent immediately for latency-sensitive cases).
  4. The user's resolved preferences determine which channels are enabled for them.
  5. If the user has disabled a channel, that channel is skipped with a reason.
  6. Quiet hours are checked — non-urgent sends during a user's quiet window are rescheduled for the end of that window; urgent sends bypass it.
  7. Each configured provider handles delivery for its channel; delivery is attempted independently per channel (no automatic fallback chain between channels).
  8. Every attempt is logged for the delivery-tracking dashboard.

Preference resolution:

Preferences are looked up per user; new users without saved preferences fall back to sensible defaults (email + push + in-app enabled, SMS off, security always on). Quiet hours are then applied to the resolved preferences before the send proceeds.

24 Built-In Transactional HTML Email Templates

HTML email layout engine:

Every built-in template is rendered through a shared HTML layout that provides:

  • CSS-inlined, responsive design (max-width 580px, mobile-friendly padding)
  • Hidden preview text tuned for inbox clients
  • A brand header bar in your configured brand color
  • Title + body content
  • A footer with unsubscribe link, preferences link, and copyright

How a handler fires:

Each of the 24 built-in handlers listens for its corresponding system event (e.g., commerce.order.shipped). When that event fires, the handler looks up the recipient's email address, renders the HTML template with the event's data, and sends it through your configured email provider — no code required. These 24 fire as a code-level baseline independent of the Unified Send API's per-user preference/quiet-hours resolution described above; they always send once their triggering event fires and a recipient email is resolvable.

All 24 handlers:

``` auth.user.registered (welcome email) auth.password.reset.requested (password reset link) auth.user.deleted (account deletion confirmation)

commerce.order.created (order confirmation) commerce.order.shipped (shipment notification) commerce.order.cancelled (cancellation confirmation) commerce.product.low_stock (low stock alert - internal)

finance.invoice.created (invoice delivery) billing.payment.failed (payment failed - action required) billing.subscription.renewal_reminder (renewal reminder - 7 days) billing.subscription.canceled (cancellation confirmation) billing.dunning.scheduled (dunning retry reminder)

helpdesk.ticket.resolved (CSAT survey) helpdesk.ticket.escalated (escalation alert) helpdesk.sla.breached (SLA breach alert to agent/manager)

recruit.interview.scheduled (interview confirmation) recruit.offer.sent (job offer details) recruit.offer.accepted (HR onboarding alert) recruit.candidate.hired (new hire welcome)

dining.reservation.confirmed (dining reservation) hospitality.reservation.confirmed (hotel reservation) smart-spaces.booking.confirmed (space booking) events.registration.confirmed (event ticket confirmation)

hr.onboarding.started (employee onboarding) ```

HTML template structure:

typescript // Example: order confirmation const orderConfirmation = <div style="text-align:center;padding:32px 0;"> <h1 style="font-size:24px;color:#1f2937;margin:0 0 16px;"> Your order is confirmed </h1> <p style="font-size:16px;color:#6b7280;margin:0 0 32px;"> Order #{{orderNumber}} · {{orderDate}} </p> </div> <table style="width:100%;border-collapse:collapse;margin:0 0 24px;"> {{#each orderItems}} <tr> <td style="padding:12px;border-bottom:1px solid #e5e7eb;"> {{name}} × {{quantity}} </td> <td style="padding:12px;border-bottom:1px solid #e5e7eb;text-align:right;"> {{price}} </td> </tr> {{/each}} </table> <a href="{{trackShipmentUrl}}" style="display:inline-block; background:#2563eb;color:#fff;padding:12px 24px;border-radius:6px; text-decoration:none;font-weight:600;"> Track Shipment </a> ;

Push Delivery — Web + Mobile

What's tracked per device:

Each push subscription records the delivery endpoint, encryption keys, browser (Chrome/Firefox/Safari), platform (web/iOS/Android), when it subscribed, when it was last used, and whether it's still active.

Web push flow:

```typescript // 1. Client subscribes via service worker (standard Web Push API) navigator.serviceWorker.ready.then((registration) => registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: VAPID_PUBLIC_KEY, }), ); // → Returns a subscription (endpoint + keys)

// 2. Send the subscription to Aeion to register the device

// 3. Aeion delivers via the standard VAPID Web Push protocol, POSTing // directly to the browser-provided endpoint (Chrome/Firefox/Safari // each route this to their own push service) with a 24h TTL

// 4. Client receives push via its service worker self.addEventListener("push", (event) => { const data = event.data.json(); event.waitUntil( self.registration.showNotification(data.title, { body: data.body, icon: data.icon, badge: data.badge, data: data.data, actions: data.actions, }), ); }); ```

Mobile push (FCM + APNs):

Android delivery goes through FCM (with topic subscriptions for broadcast sends); iOS delivery goes through APNs with token-based authentication. Both are reachable through the same send call — you don't write separate integration code per platform.

Push notification payload:

A push notification carries a title, body, optional icon/badge, a deduplication tag (groups related pushes so a newer one replaces an older one on-device), arbitrary data for deep-linking, and optional action buttons. A 24-hour TTL is applied server-side for web push delivery.

Webhook Delivery — Signed, With Retry

What your endpoint receives:

```typescript POST https://your-endpoint.example.com/webhooks Headers: Content-Type: application/json X-Notification-ID: notif_abc123 X-Notification-Type: order.shipped X-Notification-Channel: transactional X-Timestamp: 2026-05-11T10:30:00Z X-Signature: sha256=<hmac> // present when a webhook secret is configured

Body: { "id": "notif_abc123", "timestamp": "2026-05-11T10:30:00Z", "channel": "transactional", "type": "order.shipped", "priority": "default", "recipient": { "userId": "user_123", "email": "customer@example.com" }, "content": { "title": "Your order shipped", "body": "...", "actionUrl": "..." }, "data": { ... }, "metadata": { "sourceModule": "commerce", "sourceEventId": "evt_456", "groupKey": null } } ```

Every outbound webhook is signed with an HMAC-SHA256 signature over the JSON payload (when a webhook secret is configured) so your endpoint can verify authenticity before trusting the body.

Retry with exponential backoff:

```typescript // Retry schedule: 5s, 10s, 20s, 40s between attempts (5 attempts total) // Total budget: ~75 seconds // After 5 failures → routed to the dead-letter queue for manual review

// This is the same delivery queue used for every channel (email, SMS, push, // webhook) — a failure is a failure, retried uniformly regardless of status // code; there's no separate 4xx/5xx retry policy. ```

HMAC signature verification (for receiver):

typescript // Receiver verifies: const payload = JSON.stringify(body); const expected = hmacSha256(webhookSecret, payload); const received = req.headers["x-signature"]?.replace("sha256=", ""); if (expected !== received) { return { status: 401, error: "Invalid signature" }; }

Preferences — Quiet Hours & Digest Bundling

What a preference record holds:

Per user, per channel (system, transactional, social, marketing, reminders, alerts): an enabled flag, which delivery methods to use for that channel (in-app, email, push, SMS, webhook), a temporary mute-until timestamp, quiet hours (enabled flag, start/end in 24h format, IANA timezone), and a digest option (enabled flag + hourly/daily/weekly cadence) for bundling that channel's notifications instead of sending immediately. Email and push sub-settings (HTML preference, sound/vibrate/badge) round out the record.

Defaults for new users: system, transactional, social, reminders, and alerts channels are enabled by default; marketing is off (opt-in required). Default delivery methods lean on in-app + email, with push added for system/alerts, and email-only for marketing once a user opts in — none of the defaults include SMS or webhook, which users add explicitly. Quiet hours are off by default; once enabled, an unset start/end falls back to 22:00–08:00 UTC.

Quiet hours check: the current time is evaluated in the user's timezone against their configured window, correctly handling overnight spans (e.g., 22:00–08:00). If the user is inside their quiet window, only urgent, high-priority sends go through immediately — everything else is rescheduled for the end of the window.

Digest bundling: each channel preference carries a digestEnabled flag and an hourly/daily/weekly digestFrequency, so a user can indicate they'd rather receive a periodic roundup than an immediate send for that channel.

Email Webhook Tracking

Provider delivery events:

Your configured email provider (SendGrid, Mailgun, Postmark, SES) reports delivery lifecycle events back to Aeion in real time:

  • Bounce (permanent or temporary)
  • Delivered
  • Open (tracking pixel)
  • Spam complaint
  • Deferred / rejected (surfaced as a delivery failure)

Each event emits a corresponding notification.email.* event and updates that message's delivery status, so the delivery dashboard and per-recipient audit trail reflect what actually happened at the provider.

Data Model

Every notification, preference, subscription, delivery event, and template is available through the standard API and Admin UI:

  • Notifications — channel, template, priority, status (pending → queued → sent → delivered/opened/clicked/bounced/failed), scheduled time, and timestamps for each lifecycle stage.
  • Delivery log — one entry per provider event: which provider handled it, the event type, recipient, IP/user-agent (for open/click), and any failure reason — the audit trail regulators and support teams both rely on.
  • Preferences — the per-user preference record described above.
  • Push subscriptions — per-device endpoint, encryption keys, browser, platform, and activity timestamps.
  • Templates — name, trigger event, channel, delivery method, priority, subject (for email), body, action URL, and active status — fully editable from the visual template library.

Key Architecture Decisions

Email engine

Provider-agnostic — SendGrid, Mailgun, Postmark, SES, SMTP — swap without code change

Queue

Background job queue — Async delivery, retry, scheduled send, priority routing

Push

FCM + APNs unified — Web + mobile via one provider abstraction

Signature

HMAC-SHA256 — Webhook verification, integrity + authenticity

Retry

Exponential backoff — 5s, 10s, 20s, 40s — 5 attempts total (~75s budget)

Quiet hours

Timezone-aware — Respects user timezone, urgent bypasses

Digest bundling

Per-channel hourly/daily/weekly cadence — Reduce notification fatigue

Unsubscribe

One-click — GDPR compliance, instant opt-out