Skip to content
intermediate Phase 14 · Email & Notifications

Push & In-App Notifications

Implement browser push notifications, in-app notification systems, and notification preferences.

1h
0 problems
Topic Progress 0%

Introduction to Push Notifications

Push Notifications

Push notifications are messages that pop up on a user's device from a web application, even when the app is not actively open. They enable real-time engagement by delivering timely updates, alerts, and personalized content directly to users.

Key Concepts

  • Push API: The browser interface that allows applications to receive push messages from a server, even when the app is inactive or the browser is closed.
  • Service Workers: Background scripts that handle push events, display notifications, and manage notification interactions such as clicks and dismissals.
  • Notification API: The client-side API used to display desktop and mobile notifications with customizable titles, bodies, icons, and action buttons.

Why Notifications Matter

Push notifications drive user retention and engagement. Studies show push notifications can boost app engagement by up to 88% and have significantly higher open rates than email. For web applications, they bridge the gap between native apps and the browser by enabling persistent, device-level messaging.

Notification Lifecycle

1. Request Permission  ->  User grants notification access
2. Subscribe to Push   ->  Register with push service (FCM, APNs, etc.)
3. Store Subscription  ->  Save endpoint and keys on your server
4. Send Push Message   ->  Server sends payload to push service
5. Service Worker Receives  ->  Handles the push event
6. Display Notification ->  Shows the notification to the user
7. Handle Interaction   ->  Process clicks, actions, and dismissals

Types of Notifications

  • Web Push Notifications: Sent from a server via the Push API, displayed by the browser regardless of whether the site is open.
  • In-App Notifications: UI elements within the application that alert users to new messages, events, or updates while they are actively using the app.
  • Notification Center: A persistent log of past notifications that users can review and interact with after the fact.

Implementing Web Push Notifications

Web Push Notifications

Web push notifications require a Service Worker to listen for push events and the Push API to manage subscriptions. Below is a complete implementation covering registration, permission, subscription, and handling.

Registering the Service Worker

// Register service worker for push notifications
if ('serviceWorker' in navigator && 'PushManager' in window) {
  const registration = await navigator.serviceWorker.register('/sw.js');
  console.log('Service Worker registered with scope:', registration.scope);
} else {
  console.warn('Push notifications are not supported in this browser');
}

Requesting Permission

async function requestNotificationPermission() {
  const permission = await Notification.requestPermission();
  if (permission === 'granted') {
    console.log('Notification permission granted');
    return true;
  } else if (permission === 'denied') {
    console.log('Notification permission denied');
    return false;
  } else {
    console.log('Notification permission dismissed');
    return false;
  }
}

Subscribing to Push

async function subscribeToPush(registration) {
  const vapidPublicKey = 'YOUR_VAPID_PUBLIC_KEY';
  const subscription = await registration.pushManager.subscribe({
    userVisibleOnly: true,
    applicationServerKey: urlBase64ToUint8Array(vapidPublicKey)
  });

  await fetch('/api/subscribe', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ subscription })
  });
  return subscription;
}

function urlBase64ToUint8Array(base64String) {
  const padding = '='.repeat((4 - base64String.length % 4) % 4);
  const base64 = (base64String + padding)
    .replace(/-/g, '+')
    .replace(/_/g, '/');
  const rawData = window.atob(base64);
  const outputArray = new Uint8Array(rawData.length);
  for (let i = 0; i < rawData.length; ++i) {
    outputArray[i] = rawData.charCodeAt(i);
  }
  return outputArray;
}

Service Worker Push Handler

// sw.js - Handle incoming push events
self.addEventListener('push', (event) => {
  const data = event.data ? event.data.json() : {};
  const title = data.title || 'New Notification';
  const options = {
    body: data.body || 'You have a new update',
    icon: data.icon || '/images/icon-192.png',
    badge: data.badge || '/images/badge-72.png',
    data: { url: data.url || '/' },
    actions: data.actions || [
      { action: 'open', title: 'View' },
      { action: 'dismiss', title: 'Dismiss' }
    ]
  };
  event.waitUntil(self.registration.showNotification(title, options));
});

self.addEventListener('notificationclick', (event) => {
  event.notification.close();
  if (event.action === 'dismiss') return;
  const url = event.notification.data.url || '/';
  event.waitUntil(
    clients.matchAll({ type: 'window' }).then((clientList) => {
      for (const client of clientList) {
        if (client.url === url && 'focus' in client) return client.focus();
      }
      return clients.openWindow(url);
    })
  );
});

Building In-App Notification Systems

In-App Notifications

In-app notifications are UI components that inform users about events within the application. They appear while the user is actively engaged with the app and can be implemented as toast messages, banners, badges, or a notification panel.

Notification Bell Component

import { useState, useEffect } from 'react';

interface Notification {
  id: string;
  title: string;
  message: string;
  timestamp: Date;
  read: boolean;
  type: 'info' | 'success' | 'warning' | 'error';
}

function NotificationBell() {
  const [notifications, setNotifications] = useState<Notification[]>([]);
  const [isOpen, setIsOpen] = useState(false);
  const unreadCount = notifications.filter((n) => !n.read).length;

  useEffect(() => {
    const ws = new WebSocket('wss://api.example.com/notifications');
    ws.onmessage = (event) => {
      const notification = JSON.parse(event.data);
      setNotifications((prev) => [notification, ...prev]);
    };
    return () => ws.close();
  }, []);

  const markAsRead = async (id: string) => {
    await fetch(`/api/notifications/${id}/read`, { method: 'PUT' });
    setNotifications((prev) =>
      prev.map((n) => (n.id === id ? { ...n, read: true } : n))
    );
  };

  return (
    <div className="notification-container">
      <button className="notification-bell" onClick={() => setIsOpen(!isOpen)}
        aria-label={`Notifications (${unreadCount} unread)`}>
        <BellIcon />
        {unreadCount > 0 && <span className="badge">{unreadCount}</span>}
      </button>
      {isOpen && (
        <div className="notification-dropdown">
          {notifications.map((n) => (
            <div key={n.id} className={`notification-item ${n.read ? 'read' : 'unread'}`}
              onClick={() => markAsRead(n.id)}>
              <strong>{n.title}</strong>
              <p>{n.message}</p>
              <time>{n.timestamp.toLocaleTimeString()}</time>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

Server-Sent Events for Real-Time Delivery

// Server-side: SSE endpoint for notifications
app.get('/api/notifications/stream', (req, res) => {
  res.writeHead(200, {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache',
    'Connection': 'keep-alive'
  });
  const userId = req.user.id;
  const sendNotification = (data) => {
    res.write(`data: ${JSON.stringify(data)}\n\n`);
  };
  notificationService.subscribe(userId, sendNotification);
  req.on('close', () => {
    notificationService.unsubscribe(userId, sendNotification);
  });
});

// Client-side: Listening for real-time notifications
const eventSource = new EventSource('/api/notifications/stream');
eventSource.onmessage = (event) => {
  const notification = JSON.parse(event.data);
  showToast(notification.title, notification.message);
};

Toast Notification System

interface Toast {
  id: string;
  message: string;
  type: 'info' | 'success' | 'error';
  duration?: number;
}

function useToast() {
  const [toasts, setToasts] = useState<Toast[]>([]);

  const addToast = (message: string, type: Toast['type'] = 'info', duration = 5000) => {
    const id = crypto.randomUUID();
    setToasts((prev) => [...prev, { id, message, type, duration }]);
    setTimeout(() => removeToast(id), duration);
  };

  const removeToast = (id: string) => {
    setToasts((prev) => prev.filter((t) => t.id !== id));
  };

  return { toasts, addToast, removeToast };
}

Notification Preferences and User Control

Notification Preferences

Users expect control over what notifications they receive and when. A well-designed preference system respects user choices and reduces notification fatigue.

User Preference Model

interface NotificationPreferences {
  userId: string;
  channels: {
    push: boolean;
    email: boolean;
    inApp: boolean;
    sms: boolean;
  };
  categories: {
    marketing: boolean;
    security: boolean;
    product: boolean;
    social: boolean;
  };
  quietHours: {
    enabled: boolean;
    start: string; // HH:mm format
    end: string;   // HH:mm format
  };
  frequency: 'realtime' | 'hourly' | 'daily';
}

Preferences UI Component

function NotificationPreferences({ userId }: { userId: string }) {
  const [prefs, setPrefs] = useState<NotificationPreferences | null>(null);

  useEffect(() => {
    fetch(`/api/users/${userId}/notification-preferences`)
      .then((res) => res.json())
      .then(setPrefs);
  }, [userId]);

  const updatePreference = async (key: string, value: boolean | string) => {
    const updated = { ...prefs, [key]: value };
    setPrefs(updated);
    await fetch(`/api/users/${userId}/notification-preferences`, {
      method: 'PATCH',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(updated)
    });
  };

  if (!prefs) return <Spinner />;

  return (
    <div className="preferences-panel">
      <h2>Notification Preferences</h2>
      <section>
        <h3>Channels</h3>
        {Object.entries(prefs.channels).map(([channel, enabled]) => (
          <label key={channel} className="toggle-row">
            <span>{channel.charAt(0).toUpperCase() + channel.slice(1)}</span>
            <input type="checkbox" checked={enabled}
              onChange={(e) => updatePreference(`channels.${channel}`, e.target.checked)} />
          </label>
        ))}
      </section>
      <section>
        <h3>Categories</h3>
        {Object.entries(prefs.categories).map(([category, enabled]) => (
          <label key={category} className="toggle-row">
            <span>{category.charAt(0).toUpperCase() + category.slice(1)}</span>
            <input type="checkbox" checked={enabled}
              onChange={(e) => updatePreference(`categories.${category}`, e.target.checked)} />
          </label>
        ))}
      </section>
      <section>
        <h3>Quiet Hours</h3>
        <label>
          <input type="checkbox" checked={prefs.quietHours.enabled}
            onChange={(e) => updatePreference('quietHours.enabled', e.target.checked)} />
          Enable quiet hours
        </label>
        {prefs.quietHours.enabled && (
          <div className="quiet-hours-inputs">
            <input type="time" value={prefs.quietHours.start}
              onChange={(e) => updatePreference('quietHours.start', e.target.value)} />
            <span>to</span>
            <input type="time" value={prefs.quietHours.end}
              onChange={(e) => updatePreference('quietHours.end', e.target.value)} />
          </div>
        )}
      </section>
    </div>
  );
}

Server-Side Preference Enforcement

// Middleware to check user preferences before sending notification
async function sendNotification(userId, notification) {
  const prefs = await db.getUserPreferences(userId);

  // Check if category is enabled
  if (!prefs.categories[notification.category]) return;

  // Check quiet hours
  if (prefs.quietHours.enabled) {
    const now = new Date();
    const currentTime = now.toTimeString().slice(0, 5);
    if (isInRange(currentTime, prefs.quietHours.start, prefs.quietHours.end)) {
      await queueNotification(userId, notification, prefs.quietHours.end);
      return;
    }
  }

  // Send through enabled channels
  if (prefs.channels.push) await sendPush(userId, notification);
  if (prefs.channels.email) await sendEmail(userId, notification);
  if (prefs.channels.inApp) await sendInApp(userId, notification);
}

Quiz

1. What is the correct order for setting up web push notifications?

Question 1 options

2. What does VAPID stand for in the context of web push notifications?

Question 2 options

3. What is the primary benefit of implementing notification preferences with quiet hours?

Question 3 options

Flashcards

Question

What is the Push API and how does it relate to Service Workers?

Answer

The Push API enables servers to send messages to a user's device through the browser. Service Workers act as the background handler that receives these push events and decides what to do with them, such as displaying a notification. The Push API manages the subscription and message delivery, while the Service Worker handles the actual display and interaction logic.

Question

What is the difference between web push notifications and in-app notifications?

Answer

Web push notifications are delivered to the device via the Push API even when the browser is closed and require Service Workers. In-app notifications are rendered within the application UI using components like toast messages, banners, or notification panels. Web push is persistent at the device level, while in-app notifications only appear when the user is actively using the application.

Question

How does the VAPID protocol secure web push subscriptions?

Answer

VAPID (Voluntary Application Server Identification) uses public/private key pairs to authenticate the sending server. The public key is included in the push subscription sent to the browser. When sending a push message, the server signs a JSON Web Token (JWT) with its private key. The push service verifies this signature, ensuring only the authorized server can send notifications to subscribed users.

Revision Notes

Key Takeaways

  • 1. Web push notifications require three steps: register Service Worker, request permission, subscribe to Push API
  • 2. VAPID keys authenticate your server and prevent unauthorized push message delivery
  • 3. Service Workers run in the background and handle push events even when the page is closed
  • 4. In-app notifications should use WebSocket or SSE for real-time delivery with fallback polling
  • 5. Notification preferences with quiet hours and category controls reduce user opt-out rates
  • 6. Always request permission only after the user has a clear reason to receive notifications

Interview Tips

  • Explain the full push notification lifecycle from registration to interaction handling
  • Discuss why VAPID is needed and how it prevents notification spoofing
  • Describe the trade-offs between web push, in-app notifications, email, and SMS channels
  • Explain how quiet hours work and why they matter for user retention
  • Discuss WebSocket vs Server-Sent Events vs polling for real-time notification delivery
  • Be prepared to explain the Service Worker lifecycle and its role in push notifications

Cheat Sheet

Push API Setup: register SW -> requestPermission -> subscribe -> store on server. VAPID: public key in subscription, private key signs JWT for push delivery. Service Worker events: 'push' for receiving, 'notificationclick' for interactions. In-app: use WebSocket/SSE for real-time, REST as fallback. Preferences: channels (push/email/inApp/sms), categories, quiet hours, frequency control. Always explain permission rationale before requesting.