WebSockets & Real-Time Apps
Real-Time Notifications System
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:
- User notification preferences (enable/disable by type)
- Notification priority levels (low, normal, high, urgent)
- Do Not Disturb mode with scheduled quiet hours
- Notification grouping (batch similar notifications)
- Action buttons on notifications (Accept, Decline, View, etc.)
- Notification history with search and filtering
- Email digest option for missed notifications
- Integration with browser Push API for offline notifications
- Analytics tracking (delivery rate, click-through rate, dismissal rate)
Test notifications across multiple browser tabs and devices to ensure proper synchronization.