WebSockets & Real-Time Apps

Real-Time Notifications System

17 min Lesson 15 of 35

Real-Time Notifications System

Real-time notifications keep users informed about important events and updates instantly. Building a robust notification system with WebSockets enables instant delivery of alerts, updates, and messages without requiring page refreshes or polling.

Types of Real-Time Notifications

Modern applications use various notification types depending on the context and urgency:

  • Toast Notifications: Temporary pop-up messages (3-5 seconds) for non-critical updates
  • Badge Notifications: Number indicators showing unread counts on icons or menu items
  • Push Notifications: System-level notifications even when the browser is closed (using Service Workers)
  • In-App Notifications: Persistent notification centers/panels within the application
  • Sound/Vibration Alerts: Audio or haptic feedback for high-priority notifications

Server-Side Notification System

Build a comprehensive notification system on the server that manages delivery and persistence:

// server.js - Notification Server const express = require('express'); const app = express(); const http = require('http').createServer(app); const io = require('socket.io')(http); // Store notifications and user connections const notifications = new Map(); // userId => [notifications] const userSockets = new Map(); // userId => Set of socket IDs // Notification types const NotificationType = { INFO: 'info', SUCCESS: 'success', WARNING: 'warning', ERROR: 'error', MESSAGE: 'message', FRIEND_REQUEST: 'friend_request', COMMENT: 'comment', LIKE: 'like', SYSTEM: 'system' }; io.on('connection', (socket) => { const userId = socket.handshake.auth.userId; console.log(`User ${userId} connected`); // Track user socket if (!userSockets.has(userId)) { userSockets.set(userId, new Set()); } userSockets.get(userId).add(socket.id); // Send unread notifications on connection socket.emit('notificationHistory', getUserNotifications(userId, false)); // Send notification count const unreadCount = getUserNotifications(userId, false).length; socket.emit('notificationCount', unreadCount); // Mark notification as read socket.on('markNotificationRead', (notificationId) => { markAsRead(userId, notificationId); // Send updated count const unreadCount = getUserNotifications(userId, false).length; socket.emit('notificationCount', unreadCount); }); // Mark all notifications as read socket.on('markAllRead', () => { markAllAsRead(userId); socket.emit('notificationCount', 0); socket.emit('allNotificationsRead'); }); // Get notification history socket.on('getNotifications', (includeRead, callback) => { const userNotifications = getUserNotifications(userId, includeRead); callback(userNotifications); }); // Delete notification socket.on('deleteNotification', (notificationId) => { deleteNotification(userId, notificationId); const unreadCount = getUserNotifications(userId, false).length; socket.emit('notificationCount', unreadCount); }); // Handle disconnection socket.on('disconnect', () => { console.log(`User ${userId} disconnected`); const sockets = userSockets.get(userId); if (sockets) { sockets.delete(socket.id); if (sockets.size === 0) { userSockets.delete(userId); } } }); }); // Function to send notification to specific user function sendNotification(userId, notification) { const notificationData = { id: Date.now() + Math.random(), type: notification.type || NotificationType.INFO, title: notification.title, message: notification.message, data: notification.data || {}, timestamp: Date.now(), read: false, priority: notification.priority || 'normal' // low, normal, high }; // Store notification if (!notifications.has(userId)) { notifications.set(userId, []); } notifications.get(userId).unshift(notificationData); // Keep only last 100 notifications per user if (notifications.get(userId).length > 100) { notifications.get(userId).pop(); } // Send to all user's connected devices const sockets = userSockets.get(userId); if (sockets) { sockets.forEach(socketId => { io.to(socketId).emit('notification', notificationData); // Update unread count const unreadCount = getUserNotifications(userId, false).length; io.to(socketId).emit('notificationCount', unreadCount); }); } return notificationData; } // Batch notifications for efficiency function sendBatchNotifications(notifications) { notifications.forEach(({ userId, notification }) => { sendNotification(userId, notification); }); } // Get user notifications function getUserNotifications(userId, includeRead = true) { const userNotifications = notifications.get(userId) || []; if (includeRead) { return userNotifications; } return userNotifications.filter(n => !n.read); } // Mark notification as read function markAsRead(userId, notificationId) { const userNotifications = notifications.get(userId); if (userNotifications) { const notification = userNotifications.find(n => n.id === notificationId); if (notification) { notification.read = true; } } } // Mark all as read function markAllAsRead(userId) { const userNotifications = notifications.get(userId); if (userNotifications) { userNotifications.forEach(n => n.read = true); } } // Delete notification function deleteNotification(userId, notificationId) { const userNotifications = notifications.get(userId); if (userNotifications) { const index = userNotifications.findIndex(n => n.id === notificationId); if (index !== -1) { userNotifications.splice(index, 1); } } } // Example: Trigger notifications from application events app.post('/api/send-notification', express.json(), (req, res) => { const { userId, title, message, type, data } = req.body; const notification = sendNotification(userId, { type, title, message, data }); res.json({ success: true, notification }); }); const PORT = 3000; http.listen(PORT, () => { console.log(`Notification server running on port ${PORT}`); });
Note: In production, store notifications in a database (MongoDB, PostgreSQL) for persistence and better scalability.

Client-Side Notification Interface

Create an attractive and functional notification UI:

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Real-Time Notifications</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: Arial, sans-serif; background: #f5f5f5; } .navbar { background: #2c3e50; color: white; padding: 15px 30px; display: flex; justify-content: space-between; align-items: center; } .notification-bell { position: relative; cursor: pointer; font-size: 24px; } .notification-badge { position: absolute; top: -8px; right: -8px; background: #e74c3c; color: white; border-radius: 50%; width: 20px; height: 20px; font-size: 12px; display: flex; align-items: center; justify-content: center; font-weight: bold; } .notification-panel { position: absolute; top: 60px; right: 30px; width: 400px; max-height: 600px; background: white; border-radius: 8px; box-shadow: 0 4px 20px rgba(0,0,0,0.15); display: none; flex-direction: column; } .notification-panel.active { display: flex; } .panel-header { padding: 15px 20px; border-bottom: 1px solid #e0e0e0; display: flex; justify-content: space-between; align-items: center; } .mark-all-read { color: #3498db; font-size: 14px; cursor: pointer; } .notifications-list { flex: 1; overflow-y: auto; max-height: 500px; } .notification-item { padding: 15px 20px; border-bottom: 1px solid #f0f0f0; cursor: pointer; transition: background 0.2s; } .notification-item:hover { background: #f8f9fa; } .notification-item.unread { background: #e3f2fd; } .notification-item.unread::before { content: ''; display: inline-block; width: 8px; height: 8px; background: #3498db; border-radius: 50%; margin-right: 10px; } .notification-title { font-weight: bold; margin-bottom: 5px; color: #2c3e50; } .notification-message { font-size: 14px; color: #666; margin-bottom: 5px; } .notification-time { font-size: 12px; color: #999; } .notification-icon { display: inline-block; width: 30px; height: 30px; border-radius: 50%; text-align: center; line-height: 30px; margin-right: 10px; font-size: 16px; } .icon-info { background: #3498db; color: white; } .icon-success { background: #27ae60; color: white; } .icon-warning { background: #f39c12; color: white; } .icon-error { background: #e74c3c; color: white; } .icon-message { background: #9b59b6; color: white; } /* Toast Notifications */ .toast-container { position: fixed; top: 80px; right: 30px; z-index: 9999; } .toast { background: white; padding: 15px 20px; margin-bottom: 10px; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); min-width: 300px; display: flex; align-items: center; animation: slideIn 0.3s ease; } @keyframes slideIn { from { transform: translateX(400px); opacity: 0; } to { transform: translateX(0); opacity: 1; } } .toast.removing { animation: slideOut 0.3s ease; } @keyframes slideOut { to { transform: translateX(400px); opacity: 0; } } .no-notifications { padding: 40px; text-align: center; color: #999; } </style> </head> <body> <div class="navbar"> <h1>My Application</h1> <div class="notification-bell" onclick="toggleNotifications()"> 🔔 <span class="notification-badge" id="notificationBadge" style="display:none">0</span> </div> <div class="notification-panel" id="notificationPanel"> <div class="panel-header"> <h3>Notifications</h3> <span class="mark-all-read" onclick="markAllRead()">Mark all as read</span> </div> <div class="notifications-list" id="notificationsList"> <div class="no-notifications">No notifications</div> </div> </div> </div> <div class="toast-container" id="toastContainer"></div> <script src="/socket.io/socket.io.js"></script> <script src="notifications.js"></script> </body> </html>

Client-Side Notification Logic

Implement the JavaScript to handle notifications with various display options:

// notifications.js - Client-Side Notification System const userId = 'user123'; // Get from authentication const socket = io('http://localhost:3000', { auth: { userId } }); let notificationsOpen = false; let allNotifications = []; // DOM elements const notificationBadge = document.getElementById('notificationBadge'); const notificationPanel = document.getElementById('notificationPanel'); const notificationsList = document.getElementById('notificationsList'); const toastContainer = document.getElementById('toastContainer'); // Notification sound const notificationSound = new Audio('/sounds/notification.mp3'); // Receive new notification socket.on('notification', (notification) => { allNotifications.unshift(notification); // Show toast showToast(notification); // Play sound for high priority if (notification.priority === 'high') { notificationSound.play(); } // Update panel if open if (notificationsOpen) { renderNotifications(); } // Request browser notification permission if (Notification.permission === 'granted') { showBrowserNotification(notification); } }); // Update notification count socket.on('notificationCount', (count) => { updateBadge(count); }); // Receive notification history socket.on('notificationHistory', (notifications) => { allNotifications = notifications; renderNotifications(); }); // All notifications marked as read socket.on('allNotificationsRead', () => { allNotifications.forEach(n => n.read = true); renderNotifications(); }); // Show toast notification function showToast(notification) { const toast = document.createElement('div'); toast.className = 'toast'; const iconClass = `icon-${notification.type}`; const icon = getNotificationIcon(notification.type); toast.innerHTML = ` <span class="notification-icon ${iconClass}">${icon}</span> <div style="flex:1"> <div class="notification-title">${notification.title}</div> <div class="notification-message">${notification.message}</div> </div> `; toastContainer.appendChild(toast); // Auto remove after 5 seconds setTimeout(() => { toast.classList.add('removing'); setTimeout(() => toast.remove(), 300); }, 5000); // Click to dismiss toast.onclick = () => { toast.classList.add('removing'); setTimeout(() => toast.remove(), 300); }; } // Show browser notification function showBrowserNotification(notification) { new Notification(notification.title, { body: notification.message, icon: '/icon.png', badge: '/badge.png', tag: notification.id, requireInteraction: notification.priority === 'high' }); } // Toggle notification panel function toggleNotifications() { notificationsOpen = !notificationsOpen; notificationPanel.classList.toggle('active'); if (notificationsOpen) { renderNotifications(); } } // Render notifications in panel function renderNotifications() { if (allNotifications.length === 0) { notificationsList.innerHTML = '<div class="no-notifications">No notifications</div>'; return; } notificationsList.innerHTML = allNotifications.map(n => ` <div class="notification-item ${n.read ? '' : 'unread'}" onclick="handleNotificationClick('${n.id}')"> <span class="notification-icon icon-${n.type}">${getNotificationIcon(n.type)}</span> <div style="flex:1"> <div class="notification-title">${n.title}</div> <div class="notification-message">${n.message}</div> <div class="notification-time">${formatTime(n.timestamp)}</div> </div> </div> `).join(''); } // Handle notification click function handleNotificationClick(notificationId) { socket.emit('markNotificationRead', notificationId); const notification = allNotifications.find(n => n.id === notificationId); if (notification) { notification.read = true; renderNotifications(); // Handle notification action based on type if (notification.data.url) { window.location.href = notification.data.url; } } } // Mark all as read function markAllRead() { socket.emit('markAllRead'); } // Update badge function updateBadge(count) { if (count > 0) { notificationBadge.textContent = count > 99 ? '99+' : count; notificationBadge.style.display = 'flex'; } else { notificationBadge.style.display = 'none'; } } // Get icon for notification type function getNotificationIcon(type) { const icons = { info: 'ℹ️', success: '✓', warning: '⚠️', error: '✕', message: '💬', friend_request: '👤', comment: '💭', like: '❤️', system: '⚙️' }; return icons[type] || '🔔'; } // Format time function formatTime(timestamp) { const date = new Date(timestamp); const now = new Date(); const diff = now - date; if (diff < 60000) return 'Just now'; if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`; if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`; return date.toLocaleDateString(); } // Request notification permission if (Notification.permission === 'default') { Notification.requestPermission(); } // Close panel when clicking outside document.addEventListener('click', (e) => { if (!e.target.closest('.notification-bell') && !e.target.closest('.notification-panel')) { notificationsOpen = false; notificationPanel.classList.remove('active'); } });
Tip: Use Service Workers to enable push notifications even when the browser is closed. Combine WebSocket for instant delivery with Push API for offline scenarios.
Warning: Don't spam users with notifications. Implement smart batching, priority levels, and user notification preferences to avoid notification fatigue.
Exercise: Build a comprehensive notification system with:
  1. User notification preferences (enable/disable by type)
  2. Notification priority levels (low, normal, high, urgent)
  3. Do Not Disturb mode with scheduled quiet hours
  4. Notification grouping (batch similar notifications)
  5. Action buttons on notifications (Accept, Decline, View, etc.)
  6. Notification history with search and filtering
  7. Email digest option for missed notifications
  8. Integration with browser Push API for offline notifications
  9. Analytics tracking (delivery rate, click-through rate, dismissal rate)

Test notifications across multiple browser tabs and devices to ensure proper synchronization.