AboutBlogContact
Web DevelopmentApril 5, 2026 4 min read 299Updated: June 22, 2026

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

AunimedaAunimeda
📋 Table of Contents

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

How to Build an MVP in 2026: A Founder's Guide to Scope, Cost, and Speedaunimeda
Web Development

How to Build an MVP in 2026: A Founder's Guide to Scope, Cost, and Speed

What an MVP actually is (and isn't), how to scope it correctly, how much it costs in 2026, and how to choose the right development approach. Practical advice for founders and product teams.

Outsource Software Development to Kyrgyzstan: A Practical Guide for 2026aunimeda
Web Development

Outsource Software Development to Kyrgyzstan: A Practical Guide for 2026

Why businesses outsource software development to Kyrgyzstan in 2026, how to evaluate and hire a development team in Bishkek, and what the engagement process actually looks like.

SaaS Web App Development Guide 2026: Architecture, Stack, and Pricingaunimeda
Web Development

SaaS Web App Development Guide 2026: Architecture, Stack, and Pricing

How to build a SaaS web application in 2026: architecture decisions, technology stack, multi-tenancy, subscription billing, and realistic development costs. Practical guide from a development studio.

Need IT development for your business?

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

Web Development

Get Consultation All articles