AboutBlogContact
Web DevelopmentApril 5, 2026 4 min read 60

How to Build a Notification System: Push, Email, SMS Architecture

AunimedaAunimeda
📋 Table of Contents

How to Build a Notification System: Push, Email, SMS Architecture

A notification system sounds simple - "just send an email." In production: delivery failures, user preferences, rate limiting, unsubscribes, timezone awareness, and analytics make it a real engineering problem.


Notification Channels and When to Use Each

Channel Use case Urgency Open rate
Push (mobile) Activity, alerts, re-engagement High 7-10%
Push (web) Updates, promotions Medium 6-8%
Email Transactional, digests, marketing Low-Medium 20-40%
SMS OTP, critical alerts High 95%+
In-app Contextual, while using product High (user is active) 100% if seen
WhatsApp/Telegram High-engagement markets High 60-80%

Channel selection logic:

  • OTP/auth: SMS (reliability over cost)
  • Order confirmed: Email (reference document) + Push (immediacy)
  • Flash sale: Push + Email
  • Account security alert: Email + SMS
  • New message: Push (mobile) + In-app

Architecture

Event Source → Event Bus (Redis / BullMQ) → Notification Service
                                                     ↓
                                            User Preference Check
                                                     ↓
                                       Route to channel workers:
                                       - EmailWorker (Resend/SendGrid)
                                       - PushWorker (FCM/APNs)
                                       - SMSWorker (Twilio/SMS.ru)
                                       - InAppWorker (WebSocket/DB)
                                                     ↓
                                            Delivery Tracking

Decouple notification sending from business logic. The order service fires an event; it doesn't send emails directly.


The Event Model

interface NotificationEvent {
  id: string;
  type: 'order_confirmed' | 'message_received' | 'password_reset' | ...;
  userId: string;
  data: Record<string, unknown>; // Template variables
  priority: 'low' | 'medium' | 'high' | 'critical';
  channels?: string[]; // Override if specific channel required
  scheduledAt?: Date; // For delayed/digest notifications
}

Producer (order service):

await queue.add('notification', {
  type: 'order_confirmed',
  userId: order.userId,
  data: { orderId: order.id, total: order.total, items: order.items },
  priority: 'high'
});

User Preferences

Users must control what they receive. Never skip this - it's both good UX and legally required (CAN-SPAM, GDPR).

notification_preferences (
  user_id UUID,
  notification_type VARCHAR(64),
  channel VARCHAR(32), -- 'push', 'email', 'sms'
  enabled BOOLEAN DEFAULT true,
  PRIMARY KEY (user_id, notification_type, channel)
)

Before sending any notification, check preferences:

async function shouldSend(userId: string, type: string, channel: string) {
  // Critical notifications ignore preferences (OTP, security)
  if (CRITICAL_TYPES.includes(type)) return true;
  
  const pref = await db.query(
    `SELECT enabled FROM notification_preferences 
     WHERE user_id = $1 AND notification_type = $2 AND channel = $3`,
    [userId, type, channel]
  );
  
  // Default: enabled unless explicitly disabled
  return pref?.enabled ?? true;
}

Push Notifications: FCM + APNs

Android (Firebase Cloud Messaging)

import { initializeApp } from 'firebase-admin/app';
import { getMessaging } from 'firebase-admin/messaging';

const messaging = getMessaging();

async function sendPush(token: string, notification: PushPayload) {
  await messaging.send({
    token,
    notification: {
      title: notification.title,
      body: notification.body,
    },
    data: notification.data, // Custom key-value pairs
    android: {
      priority: 'high',
      ttl: 86400 * 1000, // 24 hours
    },
  });
}

iOS (APNs via FCM)

FCM handles APNs routing - you don't need a separate APNs client when using Firebase. But if sending directly to APNs:

// Using node-apn
const note = new apn.Notification();
note.alert = { title, body };
note.payload = data;
note.topic = 'com.yourapp.bundleid';
await provider.send(note, deviceToken);

Handling Invalid Tokens

Tokens expire or become invalid when users uninstall. Always handle failure responses:

try {
  await messaging.send(message);
} catch (error) {
  if (error.code === 'messaging/registration-token-not-registered') {
    // Token invalid - remove from database
    await db.deletePushToken(token);
  }
}

Email: Transactional vs Marketing

Transactional (order confirm, password reset, receipts): Use dedicated transactional providers. Don't mix with marketing email - deliverability must be protected.

  • Resend (excellent DX, modern API)
  • Postmark (excellent deliverability, strict ToS)
  • SendGrid Transactional

Marketing (newsletters, promotions): Separate IP/domain from transactional.

  • Mailchimp, Brevo, SendGrid Marketing
  • For Russia: Unisender, DashaMail

Template approach:

// Use React Email or MJML for templates
import { render } from '@react-email/render';
import OrderConfirmed from './templates/OrderConfirmed';

const html = render(<OrderConfirmed order={order} />);

await resend.emails.send({
  from: 'orders@yourcompany.com',
  to: user.email,
  subject: `Order #${order.id} confirmed`,
  html,
});

Rate Limiting Notifications

Users who receive too many notifications unsubscribe. Apply rate limits:

async function rateLimitCheck(userId: string, channel: string): Promise<boolean> {
  const key = `notif_rate:${userId}:${channel}:${getCurrentHour()}`;
  const count = await redis.incr(key);
  await redis.expire(key, 3600);
  
  const limits = { push: 5, email: 3, sms: 2 }; // Per hour
  return count <= limits[channel];
}

Digest pattern: Instead of 10 separate "new comment" emails, batch into one "You have 10 new comments" email sent daily.


Delivery Tracking

notification_logs (
  id UUID,
  user_id UUID,
  notification_type VARCHAR,
  channel VARCHAR,
  status ENUM('queued', 'sent', 'delivered', 'failed', 'opened'),
  provider_message_id TEXT,
  error_message TEXT,
  sent_at TIMESTAMPTZ,
  delivered_at TIMESTAMPTZ,
  opened_at TIMESTAMPTZ
)

Track delivery rates per channel. If push delivery drops below 80%, investigate token quality. If email open rate drops, check spam folder placement.

Build your notification system with Aunimeda →


Aunimeda develops websites and web applications for businesses — corporate sites, e-commerce, portals, and custom platforms.

Contact us to discuss your web project. See also: Web Development, E-commerce Development

Read Also

Next.js SEO Optimization in 2026: The Complete Technical Guideaunimeda
Web Development

Next.js SEO Optimization in 2026: The Complete Technical Guide

Metadata API, Open Graph, structured data, sitemap generation, Core Web Vitals, and internationalization — everything you need to rank in 2026 with the Next.js App Router.

How to Build an E-commerce Website That Actually Sells in 2026aunimeda
Web Development

How to Build an E-commerce Website That Actually Sells in 2026

The difference between an e-commerce site that converts at 1% and one that converts at 4% isn't design — it's architecture decisions made before a line of code is written. Here's the playbook.

Web Vitals & Lighthouse 100: Practical Optimization Guide 2026aunimeda
Web Development

Web Vitals & Lighthouse 100: Practical Optimization Guide 2026

Achieving Lighthouse 100 on a real-world production Next.js app - not a blank page. Covers LCP, INP (replaced FID in 2024), CLS, TTFB, font optimization, image optimization, JS bundle analysis, and CSS critical path - with specific code changes.

Need IT development for your business?

We build websites, mobile apps and AI solutions. Free consultation.

Get Consultation All articles