Build a Real-Time Notification System
Introduction
Real-time notifications are essential for modern web applications. From social media alerts to chat messages, users expect instant updates without refreshing the page.
In this tutorial, we'll build a notification system using WebSockets for real-time communication between server and clients.
- WebSocket server for real-time communication
- Notification management system
- User connection tracking
- Real-time notification delivery
- Frontend notification UI
- WebSocket fundamentals
- Socket.io library
- Real-time event handling
- User authentication with WebSockets
- Push notifications
Project Overview
Our notification system will have several key features:
Core Features
- Real-time Delivery - Instant notifications via WebSockets
- Multi-user Support - Handle multiple connected users
- Notification Types - Support different notification categories
- Offline Support - Store notifications for offline users
- Read Status - Track read/unread notifications
Prerequisites
- Node.js installed - Version 14 or higher
- Express.js - Basic knowledge
- JavaScript - ES6+ syntax
- Code editor - VS Code recommended
Project Setup
# Create project directory
mkdir notification-system
cd notification-system
# Initialize Node.js
npm init -y
# Install dependencies
npm install express socket.io cors
Project Structure
notification-system/
├── public/
│ ├── index.html
│ ├── admin.html
│ └── style.css
├── server.js
├── notifications.js
└── package.json
Server Setup
Let's create the Express server with Socket.io integration.
// server.js
const express = require('express');
const http = require('http');
const { Server } = require("socket.io");
const cors = require('cors');
const path = require('path');
const app = express();
const server = http.createServer(app);
const io = new Server(server, {
cors: {
origin: "*",
methods: ["GET", "POST"]
}
});
app.use(cors());
app.use(express.json());
app.use(express.static(path.join(__dirname, 'public')));
const NotificationManager = require('./notifications');
const notificationManager = new NotificationManager();
// Track connected users
const connectedUsers = new Map();
io.on('connection', (socket) => {
console.log(`User connected: ${socket.id}`);
socket.on('join', (userId) => {
connectedUsers.set(userId, socket.id);
socket.userId = userId;
console.log(`User ${userId} joined`);
// Send unread notifications
notificationManager.getUnreadNotifications(userId)
.then(notifications => {
socket.emit('unread_notifications', notifications);
});
});
socket.on('send_notification', (data) => {
const { recipientId, type, title, message } = data;
notificationManager.createNotification({
recipientId,
type,
title,
message
}).then(notification => {
io.emit('new_notification', notification);
// Send to specific user if online
const recipientSocket = connectedUsers.get(recipientId);
if (recipientSocket) {
io.to(recipientSocket).emit('notification', notification);
}
});
});
socket.on('mark_read', (notificationId) => {
notificationManager.markAsRead(notificationId);
socket.emit('notification_read', notificationId);
});
socket.on('disconnect', () => {
if (socket.userId) {
connectedUsers.delete(socket.userId);
console.log(`User ${socket.userId} disconnected`);
}
});
});
// REST API endpoints
app.get('/api/notifications/:userId', (req, res) => {
notificationManager.getNotifications(req.params.userId)
.then(notifications => res.json(notifications));
});
app.post('/api/notifications', (req, res) => {
const { recipientId, type, title, message } = req.body;
notificationManager.createNotification({
recipientId,
type,
title,
message
}).then(notification => {
io.emit('new_notification', notification);
res.json(notification);
});
});
const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
WebSocket Implementation
The notification manager handles storing and retrieving notifications.
// notifications.js
class NotificationManager {
constructor() {
this.notifications = [];
this.notificationId = 0;
}
createNotification(data) {
const notification = {
id: ++this.notificationId,
recipientId: data.recipientId,
type: data.type || 'info',
title: data.title,
message: data.message,
read: false,
createdAt: new Date()
};
this.notifications.push(notification);
return Promise.resolve(notification);
}
getNotifications(userId) {
const userNotifications = this.notifications
.filter(n => n.recipientId === userId)
.sort((a, b) => b.createdAt - a.createdAt);
return Promise.resolve(userNotifications);
}
getUnreadNotifications(userId) {
const unread = this.notifications
.filter(n => n.recipientId === userId && !n.read)
.sort((a, b) => b.createdAt - a.createdAt);
return Promise.resolve(unread);
}
markAsRead(notificationId) {
const notification = this.notifications.find(n => n.id === notificationId);
if (notification) {
notification.read = true;
}
return Promise.resolve(notification);
}
markAllAsRead(userId) {
this.notifications
.filter(n => n.recipientId === userId)
.forEach(n => n.read = true);
return Promise.resolve();
}
deleteNotification(notificationId) {
const index = this.notifications.findIndex(n => n.id === notificationId);
if (index > -1) {
this.notifications.splice(index, 1);
}
return Promise.resolve();
}
}
module.exports = NotificationManager;
- join - User joins, receives unread notifications
- send_notification - Send a new notification
- mark_read - Mark notification as read
- new_notification - Broadcast to all users
- notification - Direct message to user
Notification System Features
Let's add more advanced notification features.
// Advanced notification features
class AdvancedNotificationManager {
constructor() {
this.notifications = [];
this.subscribers = new Map();
}
// Create notification with priority
createPriorityNotification(data) {
const notification = {
id: Date.now(),
recipientId: data.recipientId,
type: data.type,
priority: data.priority || 'normal',
title: data.title,
message: data.message,
read: false,
archived: false,
createdAt: new Date(),
expiresAt: data.expiresAt || null
};
this.notifications.push(notification);
this.notifySubscribers(notification);
return notification;
}
// Subscribe to notification types
subscribe(userId, callback, types = []) {
if (!this.subscribers.has(userId)) {
this.subscribers.set(userId, []);
}
this.subscribers.get(userId).push({ callback, types });
}
unsubscribe(userId) {
this.subscribers.delete(userId);
}
notifySubscribers(notification) {
this.subscribers.forEach((subs, userId) => {
subs.forEach(sub => {
if (sub.types.includes(notification.type) || sub.types.length === 0) {
sub.callback(notification);
}
});
});
}
// Get notification statistics
getStats(userId) {
const userNotifs = this.notifications.filter(n => n.recipientId === userId);
return {
total: userNotifs.length,
unread: userNotifs.filter(n => !n.read).length,
byType: this.groupBy(userNotifs, 'type'),
byPriority: this.groupBy(userNotifs, 'priority')
};
}
groupBy(arr, key) {
return arr.reduce((acc, item) => {
acc[item[key]] = (acc[item[key]] || 0) + 1;
return acc;
}, {});
}
// Clean up expired notifications
cleanupExpired() {
const now = new Date();
this.notifications = this.notifications.filter(n =>
!n.expiresAt || new Date(n.expiresAt) > now
);
}
}
Building the Frontend
Let's create the client-side code to receive real-time notifications.
<!-- public/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Notifications</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="container">
<h1>Notifications</h1>
<div class="user-info">
<input type="text" id="userId" placeholder="Enter your user ID" value="user1">
<button onclick="join()" class="btn">Join</button>
</div>
<div class="notification-panel">
<div class="notification-header">
<h2>Notifications</h2>
<span id="unreadCount" class="badge">0</span>
<button onclick="markAllRead()" class="btn-small">Mark all read</button>
</div>
<div id="notifications" class="notification-list"></div>
</div>
</div>
<script src="/socket.io/socket.io.js"></script>
<script>
const socket = io();
let currentUser = null;
function join() {
currentUser = document.getElementById('userId').value;
socket.emit('join', currentUser);
loadNotifications();
}
function loadNotifications() {
fetch(\`/api/notifications/\${currentUser}\`)
.then(res => res.json())
.then(notifications => displayNotifications(notifications));
}
function displayNotifications(notifications) {
const container = document.getElementById('notifications');
const unreadCount = notifications.filter(n => !n.read).length;
document.getElementById('unreadCount').textContent = unreadCount;
container.innerHTML = notifications.map(notif => \`
<div class="notification \${notif.read ? 'read' : 'unread'}"
data-id="\${notif.id}">
<div class="notification-type"\>\${notif.type}</div>
<h3>\${notif.title}</h3>
<p>\${notif.message}</p>
<small>\${new Date(notif.createdAt).toLocaleString()}</small>
<button onclick="markRead(\${notif.id})" class="btn-small">
\${notif.read ? 'Read' : 'Mark Read'}
</button>
</div>
\`).join('');
}
function markRead(id) {
socket.emit('mark_read', id);
const el = document.querySelector(\`[data-id="\${id}"]\`);
el.classList.remove('unread');
el.classList.add('read');
updateUnreadCount();
}
function markAllRead() {
fetch(\`/api/notifications/\${currentUser}\`)
.then(res => res.json())
.then(notifications => {
notifications.forEach(n => {
if (!n.read) markRead(n.id);
});
});
}
function updateUnreadCount() {
const unread = document.querySelectorAll('.notification.unread').length;
document.getElementById('unreadCount').textContent = unread;
}
// Socket event listeners
socket.on('new_notification', (notification) => {
if (notification.recipientId === currentUser) {
addNotificationToList(notification);
}
});
socket.on('notification', (notification) => {
addNotificationToList(notification);
});
function addNotificationToList(notification) {
const container = document.getElementById('notifications');
const html = \`
<div class="notification unread" data-id="\${notification.id}">
<div class="notification-type"\>\${notification.type}</div>
<h3>\${notification.title}</h3>
<p>\${notification.message}</p>
<small>\${new Date().toLocaleString()}</small>
</div>
\`;
container.insertAdjacentHTML('afterbegin', html);
updateUnreadCount();
// Show browser notification
if (Notification.permission === 'granted') {
new Notification(notification.title, { body: notification.message });
}
}
// Request notification permission
if ('Notification' in window && Notification.permission !== 'granted') {
Notification.requestPermission();
}
</script>
</body>
</html>
You can use the browser's Notification API to show desktop notifications even when the tab is not active.
Testing
# Start the server
node server.js
# Test with multiple browser tabs
# 1. Open http://localhost:3000 in two tabs
# 2. Enter different user IDs in each tab
# 3. Send notification via API
# Send notification via curl
curl -X POST http://localhost:3000/api/notifications \
-H "Content-Type: application/json" \
-d '{
"recipientId": "user1",
"type": "info",
"title": "New Message",
"message": "You have a new message!"
}'
- ✓ User can join and receive notifications
- ✓ Real-time delivery works
- ✓ Mark as read works
- ✓ Multiple users supported
- ✓ Browser notifications appear
Summary
Congratulations! You've built a complete real-time notification system.
What You Built
- WebSocket Server - Real-time communication
- Notification Manager - Store and manage notifications
- Real-time Delivery - Instant push notifications
- Frontend UI - User notification panel
Next Steps
- Add notification categories (email, SMS)
- Implement notification preferences
- Add database persistence
- Implement notification grouping