Understanding Real-time Notifications
Real-time notifications have become an essential element of modern web applications. Think about how you receive instant notifications when someone messages you on social media, when your food delivery status changes, or when a new email arrives in your inbox - all without refreshing the page.
Today, we'll explore how to implement these real-time notification systems using WebSockets and Socket.io, with a focus on creating responsive, interactive user experiences.
Why Real-time Notifications Matter
Traditional web applications relied on the request-response cycle: the client makes a request, the server responds, and the process repeats. To get updated information, users had to manually refresh their page or the application had to poll the server frequently. This approach has several limitations:
- Poor User Experience: Users don't know when new information is available
- Inefficient Resource Usage: Polling the server frequently wastes bandwidth and server resources
- Delayed Information: Updates aren't truly real-time, causing lag between events and notifications
Real-time notifications solve these problems by establishing persistent connections that allow servers to push updates to clients immediately when events occur. This creates more engaging, responsive applications that feel alive and dynamic.
Real World Applications
- Social Media: New messages, likes, comments, and friend requests
- E-commerce: Order status updates, price drops, limited-time offers
- Collaboration Tools: Document edits, comments, new assignments
- Financial Apps: Stock price changes, transaction alerts
- IoT Applications: Smart home device status changes
Technologies for Real-time Communication
Several approaches have been used to achieve real-time communication, each with different advantages and limitations:
flowchart TB
RT[Real-time Communication Techniques]
P[Polling]
LP[Long Polling]
SSE[Server-Sent Events]
WS[WebSockets]
RT --> P
RT --> LP
RT --> SSE
RT --> WS
P -->|"Client requests at intervals
High overhead
Not truly real-time"| PE[Regular Polling]
LP -->|"Client holds connection open
Server responds when data available
Less efficient than WebSockets"| LPE[Long Polling]
SSE -->|"Server-to-client only
Over HTTP
Built-in reconnection"| SSEE[EventSource API]
WS -->|"Full-duplex
Persistent connection
Low latency"| WSI[WebSocket API]
Today, we'll focus on WebSockets, the most robust solution for true real-time bidirectional communication, and Socket.io, a library that makes WebSocket implementation easier and more reliable.
Understanding WebSockets
WebSockets provide a persistent connection between client and server, allowing both parties to send data at any time without the overhead of HTTP requests. This makes WebSockets ideal for applications requiring low latency and frequent updates.
sequenceDiagram
participant Client
participant Server
Note over Client,Server: HTTP Handshake
Client->>Server: HTTP Request with Upgrade header
Server->>Client: HTTP 101 Switching Protocols
Note over Client,Server: WebSocket Connection Established
Client->>Server: WebSocket message
Server->>Client: WebSocket message
Note over Server: Event occurs
Server->>Client: Push notification without client request
Client->>Server: WebSocket message
Note over Client,Server: Either side can initiate communication
Key WebSocket Characteristics
- Full-duplex communication: Both client and server can send messages independently
- Persistent connection: No need to establish new connections for each message
- Lower overhead: After initial handshake, minimal header data is transmitted
- Cross-domain communication: WebSockets aren't restricted by same-origin policy
Introducing Socket.io
While the native WebSocket API is powerful, Socket.io offers several advantages that make it the preferred choice for many developers:
- Fallback mechanisms: Automatically falls back to alternative transport methods when WebSockets aren't available
- Reconnection support: Handles connection drops and automatically attempts to reconnect
- Room support: Easily group connections for targeted broadcasting
- Event-based architecture: Intuitive API for sending and receiving events
- Binary data support: Seamlessly transmit binary data like images or files
Basic Socket.io Setup
Server-side (Node.js with Express)
// server.js
const express = require('express');
const http = require('http');
const { Server } = require('socket.io');
const app = express();
const server = http.createServer(app);
const io = new Server(server);
// Serve static files
app.use(express.static('public'));
// Socket.io connection handling
io.on('connection', (socket) => {
console.log('A user connected:', socket.id);
// Listen for events from this client
socket.on('join-room', (room) => {
socket.join(room);
console.log(`User ${socket.id} joined room: ${room}`);
});
// Listen for disconnect
socket.on('disconnect', () => {
console.log('User disconnected:', socket.id);
});
});
server.listen(3000, () => {
console.log('Server listening on port 3000');
});
Client-side
// In your HTML file
<script src="/socket.io/socket.io.js"></script>
<script>
// Connect to the Socket.io server
const socket = io();
// Listen for connection event
socket.on('connect', () => {
console.log('Connected to server with ID:', socket.id);
// Join a specific room
socket.emit('join-room', 'notifications');
});
// Listen for disconnection
socket.on('disconnect', () => {
console.log('Disconnected from server');
});
</script>
Building a Notification System
Now, let's build a practical notification system using Socket.io. We'll create a system that can support different types of notifications and deliver them to specific users or groups.
flowchart LR
E[Event Occurs] --> BE[Backend Service]
BE --> N[Notification Service]
N --> S[Socket.io Server]
S --> R{Routes to Recipients}
R --> U1[User 1]
R --> U2[User 2]
R --> G[Group/Room]
G --> U3[User 3]
G --> U4[User 4]
Server-side Notification System
// notification-service.js
class NotificationService {
constructor(io) {
this.io = io;
this.userSockets = new Map(); // Map user IDs to socket IDs
}
// Register a user connection
registerUser(userId, socketId) {
if (!this.userSockets.has(userId)) {
this.userSockets.set(userId, new Set());
}
this.userSockets.get(userId).add(socketId);
console.log(`User ${userId} registered with socket ${socketId}`);
}
// Unregister a socket
unregisterSocket(socketId) {
for (const [userId, sockets] of this.userSockets.entries()) {
if (sockets.has(socketId)) {
sockets.delete(socketId);
console.log(`Socket ${socketId} unregistered from user ${userId}`);
if (sockets.size === 0) {
this.userSockets.delete(userId);
console.log(`User ${userId} has no active connections`);
}
break;
}
}
}
// Send notification to a specific user
sendToUser(userId, notificationType, payload) {
if (this.userSockets.has(userId)) {
const socketIds = this.userSockets.get(userId);
for (const socketId of socketIds) {
this.io.to(socketId).emit('notification', {
type: notificationType,
data: payload,
timestamp: new Date()
});
}
console.log(`Notification sent to user ${userId}`);
return socketIds.size; // Return number of active connections
}
return 0; // No active connections for this user
}
// Send notification to a room/group
sendToRoom(room, notificationType, payload) {
this.io.to(room).emit('notification', {
type: notificationType,
data: payload,
timestamp: new Date()
});
console.log(`Notification sent to room ${room}`);
}
// Broadcast notification to all connected clients
broadcast(notificationType, payload) {
this.io.emit('notification', {
type: notificationType,
data: payload,
timestamp: new Date()
});
console.log('Notification broadcasted to all users');
}
}
module.exports = NotificationService;
Integration with Express and Socket.io
// server.js
const express = require('express');
const http = require('http');
const { Server } = require('socket.io');
const NotificationService = require('./notification-service');
const app = express();
const server = http.createServer(app);
const io = new Server(server);
// Initialize notification service
const notificationService = new NotificationService(io);
// API routes for triggering notifications
app.use(express.json());
// Route to send notification to specific user
app.post('/api/notifications/user/:userId', (req, res) => {
const { userId } = req.params;
const { type, data } = req.body;
const sentCount = notificationService.sendToUser(userId, type, data);
if (sentCount > 0) {
res.status(200).json({ success: true, sentTo: sentCount });
} else {
res.status(404).json({ success: false, message: 'User not connected' });
}
});
// Route to send notification to a room
app.post('/api/notifications/room/:roomName', (req, res) => {
const { roomName } = req.params;
const { type, data } = req.body;
notificationService.sendToRoom(roomName, type, data);
res.status(200).json({ success: true });
});
// Socket.io connection handling
io.on('connection', (socket) => {
console.log('A user connected:', socket.id);
// Authenticate and register user
socket.on('auth', (userData) => {
// In a real app, you'd verify the user authentication here
const userId = userData.userId;
// Register this socket with the user ID
notificationService.registerUser(userId, socket.id);
// Join user to their personal room
socket.join(`user:${userId}`);
// Acknowledge successful authentication
socket.emit('auth_success', { userId });
});
// Join a room/group
socket.on('join-room', (room) => {
socket.join(room);
console.log(`Socket ${socket.id} joined room: ${room}`);
// Notify others in the room
socket.to(room).emit('notification', {
type: 'user-joined',
data: { roomName: room },
timestamp: new Date()
});
});
// Listen for disconnect
socket.on('disconnect', () => {
console.log('User disconnected:', socket.id);
notificationService.unregisterSocket(socket.id);
});
});
server.listen(3000, () => {
console.log('Server listening on port 3000');
});
Client-side Notification Handling
class NotificationClient {
constructor() {
this.socket = io();
this.notificationHandlers = new Map();
this.userId = null;
// Set up socket event listeners
this.setupSocketListeners();
}
setupSocketListeners() {
// Connection established
this.socket.on('connect', () => {
console.log('Connected to notification server');
// If we have cached user ID, authenticate
const cachedUserId = localStorage.getItem('userId');
if (cachedUserId) {
this.authenticate(cachedUserId);
}
});
// Handle authentication success
this.socket.on('auth_success', (data) => {
console.log('Authentication successful:', data);
this.userId = data.userId;
localStorage.setItem('userId', data.userId);
// Dispatch an event for the UI
document.dispatchEvent(new CustomEvent('notifications:ready'));
});
// Handle incoming notifications
this.socket.on('notification', (notification) => {
console.log('Received notification:', notification);
// Play sound for new notifications
this.playNotificationSound();
// Store notification in history
this.storeNotification(notification);
// Handle based on notification type
if (this.notificationHandlers.has(notification.type)) {
this.notificationHandlers.get(notification.type)(notification);
}
// Dispatch generic notification event for the UI
document.dispatchEvent(new CustomEvent('notifications:new', {
detail: notification
}));
});
// Handle disconnection
this.socket.on('disconnect', () => {
console.log('Disconnected from notification server');
document.dispatchEvent(new CustomEvent('notifications:offline'));
});
}
// Authenticate with the notification server
authenticate(userId) {
this.socket.emit('auth', { userId });
}
// Join a notification room/channel
joinRoom(roomName) {
this.socket.emit('join-room', roomName);
}
// Register a handler for a specific notification type
registerHandler(notificationType, handlerFunction) {
this.notificationHandlers.set(notificationType, handlerFunction);
}
// Play a sound when notification arrives
playNotificationSound() {
const audio = new Audio('/sounds/notification.mp3');
audio.play().catch(err => console.log('Could not play notification sound', err));
}
// Store notification in browser storage
storeNotification(notification) {
const notifications = JSON.parse(localStorage.getItem('notifications') || '[]');
notifications.push(notification);
// Keep only the latest 50 notifications
if (notifications.length > 50) {
notifications.shift();
}
localStorage.setItem('notifications', JSON.stringify(notifications));
}
// Get notification history
getNotificationHistory() {
return JSON.parse(localStorage.getItem('notifications') || '[]');
}
}
// Usage example:
document.addEventListener('DOMContentLoaded', () => {
const notificationClient = new NotificationClient();
// Register handlers for different notification types
notificationClient.registerHandler('message', (notification) => {
showMessageNotification(notification.data);
});
notificationClient.registerHandler('friend-request', (notification) => {
showFriendRequestNotification(notification.data);
});
// Function to display notification in UI
function showMessageNotification(data) {
const notificationElement = document.createElement('div');
notificationElement.className = 'notification message-notification';
notificationElement.innerHTML = `
<div class="notification-icon">
<i class="fas fa-envelope"></i>
</div>
<div class="notification-content">
<h4>New Message from ${data.sender}</h4>
<p>${data.preview}</p>
</div>
<div class="notification-actions">
<button class="view-btn">View</button>
</div>
`;
document.getElementById('notification-container').appendChild(notificationElement);
// Auto-remove after 5 seconds
setTimeout(() => {
notificationElement.classList.add('notification-hiding');
setTimeout(() => notificationElement.remove(), 500);
}, 5000);
}
// Function to display friend request notification
function showFriendRequestNotification(data) {
// Similar implementation...
}
// When the login button is clicked
document.getElementById('login-btn').addEventListener('click', () => {
const userId = document.getElementById('user-id').value;
notificationClient.authenticate(userId);
});
});
Designing the Notification UI
A good notification system needs an effective UI. Let's explore some design patterns for displaying notifications:
Common Notification UI Patterns
- Toast Notifications: Small, non-intrusive messages that appear briefly and disappear automatically
- Notification Center: A centralized location where users can view all their notifications
- Badge Counters: Small numbered indicators showing unread notification count
- Push Notifications: System-level notifications that appear even when the app isn't active
HTML/CSS for Toast Notifications
<!-- HTML structure for toast notifications -->
<div id="toast-container"></div>
<!-- CSS for notifications -->
<style>
#toast-container {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 1000;
}
.toast-notification {
background-color: #fff;
border-left: 4px solid #4caf50;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
border-radius: 4px;
padding: 16px;
margin-bottom: 10px;
display: flex;
align-items: center;
min-width: 300px;
max-width: 400px;
animation: slideIn 0.3s, fadeIn 0.3s;
}
.toast-notification.hiding {
animation: slideOut 0.3s, fadeOut 0.3s;
}
.toast-notification.error {
border-left-color: #f44336;
}
.toast-notification.warning {
border-left-color: #ff9800;
}
.toast-notification.info {
border-left-color: #2196f3;
}
.notification-icon {
margin-right: 12px;
font-size: 20px;
width: 24px;
text-align: center;
}
.notification-content {
flex-grow: 1;
}
.notification-content h4 {
margin: 0 0 4px 0;
font-size: 16px;
}
.notification-content p {
margin: 0;
color: #666;
font-size: 14px;
}
.notification-close {
color: #999;
background: none;
border: none;
cursor: pointer;
font-size: 16px;
}
@keyframes slideIn {
from { transform: translateX(100%); }
to { transform: translateX(0); }
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slideOut {
from { transform: translateX(0); }
to { transform: translateX(100%); }
}
@keyframes fadeOut {
from { opacity: 1; }
to { opacity: 0; }
}
</style>
JavaScript for Toast Notifications
class ToastNotification {
constructor(options = {}) {
this.container = document.getElementById(options.containerId || 'toast-container');
if (!this.container) {
this.container = document.createElement('div');
this.container.id = 'toast-container';
document.body.appendChild(this.container);
}
this.defaultDuration = options.defaultDuration || 5000;
}
show(options) {
const { type = 'info', title, message, icon, duration = this.defaultDuration } = options;
// Create notification element
const notification = document.createElement('div');
notification.className = `toast-notification ${type}`;
// Add content
notification.innerHTML = `
${icon ? `<div class="notification-icon">${icon}</div>` : ''}
<div class="notification-content">
${title ? `<h4>${title}</h4>` : ''}
${message ? `<p>${message}</p>` : ''}
</div>
<button class="notification-close" aria-label="Close">×</button>
`;
// Add to container
this.container.appendChild(notification);
// Add close button handler
const closeButton = notification.querySelector('.notification-close');
closeButton.addEventListener('click', () => this.close(notification));
// Set auto-close timer
if (duration) {
setTimeout(() => this.close(notification), duration);
}
return notification;
}
close(notification) {
notification.classList.add('hiding');
setTimeout(() => {
if (notification.parentNode) {
notification.parentNode.removeChild(notification);
}
}, 300); // Match the animation duration
}
// Convenience methods for different notification types
success(options) {
return this.show({
...options,
type: 'success',
icon: '<i class="fas fa-check-circle"></i>'
});
}
error(options) {
return this.show({
...options,
type: 'error',
icon: '<i class="fas fa-exclamation-circle"></i>'
});
}
warning(options) {
return this.show({
...options,
type: 'warning',
icon: '<i class="fas fa-exclamation-triangle"></i>'
});
}
info(options) {
return this.show({
...options,
type: 'info',
icon: '<i class="fas fa-info-circle"></i>'
});
}
}
// Usage example
const toast = new ToastNotification();
// Connect to our notification client
document.addEventListener('notifications:new', (event) => {
const notification = event.detail;
switch (notification.type) {
case 'message':
toast.info({
title: `New message from ${notification.data.sender}`,
message: notification.data.preview
});
break;
case 'friend-request':
toast.success({
title: 'Friend Request',
message: `${notification.data.name} wants to connect`
});
break;
case 'error':
toast.error({
title: 'Error',
message: notification.data.message
});
break;
default:
toast.info({
title: notification.type,
message: JSON.stringify(notification.data)
});
}
});
Real-world Integration Examples
E-commerce Order Updates
// Server-side event handler for order status changes
function handleOrderStatusChange(order) {
const { orderId, userId, status, estimatedDelivery } = order;
// Send notification to the customer
notificationService.sendToUser(userId, 'order-update', {
orderId,
status,
estimatedDelivery,
message: getOrderStatusMessage(status),
timestamp: new Date()
});
// If it's a delivery status, also notify the delivery team
if (status === 'out-for-delivery') {
notificationService.sendToRoom('delivery-team', 'new-delivery', {
orderId,
customer: {
id: userId,
address: order.shippingAddress
},
estimatedDelivery
});
}
}
function getOrderStatusMessage(status) {
switch (status) {
case 'processing':
return 'Your order is being processed.';
case 'shipped':
return 'Great news! Your order has been shipped.';
case 'out-for-delivery':
return 'Your order is out for delivery.';
case 'delivered':
return 'Your order has been delivered. Enjoy!';
default:
return `Your order status is: ${status}`;
}
}
// Integrate with order processing system
orderProcessor.on('status-change', handleOrderStatusChange);
Chat Application
// Server-side message handler
io.on('connection', (socket) => {
// ... other connection handling
// Handle new messages
socket.on('send-message', async (data) => {
const { roomId, message, sender } = data;
try {
// Save message to database
const savedMessage = await chatService.saveMessage({
roomId,
sender,
content: message,
timestamp: new Date()
});
// Broadcast to room
socket.to(roomId).emit('notification', {
type: 'new-message',
data: {
messageId: savedMessage.id,
roomId,
sender,
preview: message.substring(0, 50) + (message.length > 50 ? '...' : ''),
timestamp: savedMessage.timestamp
}
});
// For users not in the room but part of the conversation, send a different notification
const roomMembers = await chatService.getRoomMembers(roomId);
for (const memberId of roomMembers) {
if (memberId !== sender) {
// Check if user is online but not in this room
if (notificationService.isUserOnline(memberId) &&
!notificationService.isUserInRoom(memberId, roomId)) {
notificationService.sendToUser(memberId, 'unread-message', {
messageId: savedMessage.id,
roomId,
sender,
preview: message.substring(0, 50) + (message.length > 50 ? '...' : ''),
timestamp: savedMessage.timestamp
});
}
}
}
socket.emit('message-sent', { id: savedMessage.id, status: 'sent' });
} catch (error) {
console.error('Error sending message:', error);
socket.emit('message-error', { error: 'Failed to send message' });
}
});
});
Collaborative Document Editing
// Server-side document edit handler
io.on('connection', (socket) => {
// ... other connection handling
// Handle document edits
socket.on('document-change', (data) => {
const { documentId, change, user } = data;
// Save the change to the database
documentService.saveChange(documentId, change, user)
.then(() => {
// Broadcast to everyone editing the document except sender
socket.to(`document:${documentId}`).emit('notification', {
type: 'document-change',
data: {
documentId,
change,
user,
timestamp: new Date()
}
});
// Notify document owner if they're not currently editing
return documentService.getDocumentOwner(documentId);
})
.then(ownerId => {
if (ownerId !== user.id && !notificationService.isUserInRoom(ownerId, `document:${documentId}`)) {
notificationService.sendToUser(ownerId, 'document-edited', {
documentId,
editorName: user.name,
documentName: data.documentName,
timestamp: new Date()
});
}
})
.catch(error => {
console.error('Error handling document change:', error);
socket.emit('document-error', { error: 'Failed to save changes' });
});
});
// Handle document comments
socket.on('add-comment', (data) => {
const { documentId, comment, user, mentionedUsers } = data;
// Save comment to database
documentService.saveComment(documentId, comment, user)
.then(savedComment => {
// Broadcast to document room
socket.to(`document:${documentId}`).emit('notification', {
type: 'new-comment',
data: {
documentId,
comment: savedComment,
user,
timestamp: new Date()
}
});
// Send specific notifications to mentioned users
if (mentionedUsers && mentionedUsers.length > 0) {
for (const mentionedUser of mentionedUsers) {
notificationService.sendToUser(mentionedUser.id, 'user-mention', {
documentId,
commentId: savedComment.id,
mentionedBy: user.name,
documentName: data.documentName,
timestamp: new Date()
});
}
}
return savedComment;
})
.then(savedComment => {
socket.emit('comment-added', { id: savedComment.id, status: 'added' });
})
.catch(error => {
console.error('Error adding comment:', error);
socket.emit('comment-error', { error: 'Failed to add comment' });
});
});
});
Best Practices for Real-time Notifications
Performance Considerations
- Throttle notifications: Avoid overwhelming users with too many notifications at once
- Group similar notifications: Combine related notifications to reduce noise
- Use binary data formats: Consider MessagePack or Protocol Buffers for high-volume systems
- Implement horizontal scaling: Use Redis adapter to scale Socket.io across multiple servers
Design Considerations
- Respect user preferences: Allow users to control which notifications they receive
- Provide clear context: Ensure notifications include enough information to be useful
- Design for accessibility: Make notifications accessible to all users
- Consider mobile contexts: Design notifications that work well on small screens
Security Considerations
- Authenticate before registering: Verify user identity before connecting them to notification system
- Validate payload data: Sanitize notification content to prevent XSS attacks
- Implement rate limiting: Prevent abuse and DoS attacks
- Use namespaces and rooms: Control access to specific notification channels
Socket.io with Redis Adapter for Scaling
// server.js
const express = require('express');
const http = require('http');
const { Server } = require('socket.io');
const { createAdapter } = require('@socket.io/redis-adapter');
const { createClient } = require('redis');
const app = express();
const server = http.createServer(app);
const io = new Server(server);
// Set up Redis adapter for horizontal scaling
const pubClient = createClient({ url: process.env.REDIS_URL });
const subClient = pubClient.duplicate();
Promise.all([pubClient.connect(), subClient.connect()]).then(() => {
io.adapter(createAdapter(pubClient, subClient));
console.log('Socket.io connected to Redis adapter');
});
// ... rest of your Socket.io setup
Notification Preference Management
// User preference schema
const notificationPreferenceSchema = new mongoose.Schema({
userId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
},
preferences: {
email: {
type: Boolean,
default: true
},
push: {
type: Boolean,
default: true
},
inApp: {
type: Boolean,
default: true
}
},
channels: {
messages: {
type: Boolean,
default: true
},
friendRequests: {
type: Boolean,
default: true
},
comments: {
type: Boolean,
default: true
},
// Add more notification channels as needed
},
doNotDisturbMode: {
enabled: {
type: Boolean,
default: false
},
startTime: {
type: String,
default: "22:00" // 10 PM
},
endTime: {
type: String,
default: "08:00" // 8 AM
},
timezone: {
type: String,
default: "UTC"
}
}
});
// Enhanced notification service that respects user preferences
function shouldSendNotification(userId, notificationType) {
return UserPreference.findOne({ userId })
.then(preferences => {
if (!preferences) {
return true; // Default to sending if no preferences set
}
// Check if user is in do-not-disturb mode
if (preferences.doNotDisturbMode.enabled) {
const now = new Date();
const userTz = preferences.doNotDisturbMode.timezone;
const userTime = new Date(now.toLocaleString('en-US', { timeZone: userTz }));
const userHour = userTime.getHours();
const userMinute = userTime.getMinutes();
const startParts = preferences.doNotDisturbMode.startTime.split(':');
const endParts = preferences.doNotDisturbMode.endTime.split(':');
const startHour = parseInt(startParts[0], 10);
const startMinute = parseInt(startParts[1], 10);
const endHour = parseInt(endParts[0], 10);
const endMinute = parseInt(endParts[1], 10);
const currentTime = userHour * 60 + userMinute;
const startTime = startHour * 60 + startMinute;
const endTime = endHour * 60 + endMinute;
if (startTime < endTime) {
// Normal time range (e.g., 22:00 to 08:00)
if (currentTime >= startTime && currentTime <= endTime) {
return false; // In do-not-disturb time range
}
} else {
// Overnight time range (e.g., 22:00 to 08:00)
if (currentTime >= startTime || currentTime <= endTime) {
return false; // In do-not-disturb time range
}
}
}
// Check if user has enabled in-app notifications
if (!preferences.preferences.inApp) {
return false;
}
// Map notification type to preference channel
const channelMap = {
'message': 'messages',
'friend-request': 'friendRequests',
'comment': 'comments',
// Map other notification types to preference channels
};
const preferenceChannel = channelMap[notificationType] || notificationType;
// Check if this notification channel is enabled
return preferences.channels[preferenceChannel] !== false;
})
.catch(error => {
console.error('Error checking notification preferences:', error);
return true; // Default to sending if there's an error
});
}
// Enhance the notification service
class EnhancedNotificationService extends NotificationService {
async sendToUser(userId, notificationType, payload) {
const shouldSend = await shouldSendNotification(userId, notificationType);
if (shouldSend) {
return super.sendToUser(userId, notificationType, payload);
}
return 0; // Notification suppressed due to user preferences
}
}
Advanced Topics in Real-time Notifications
Push Notifications
Extend your real-time notification system to include push notifications using Web Push API or services like Firebase Cloud Messaging (FCM).
// Using web-push library for Web Push API
const webpush = require('web-push');
// VAPID keys should be generated only once
const vapidKeys = webpush.generateVAPIDKeys();
webpush.setVapidDetails(
'mailto:example@yourdomain.com',
vapidKeys.publicKey,
vapidKeys.privateKey
);
// Store user's subscription object
app.post('/api/push-subscribe', async (req, res) => {
const { subscription, userId } = req.body;
try {
await saveSubscription(userId, subscription);
res.status(201).json({ success: true });
} catch (error) {
console.error('Error saving push subscription:', error);
res.status(500).json({ error: 'Failed to save subscription' });
}
});
// Send push notification
async function sendPushNotification(userId, title, body, data = {}) {
try {
const subscriptions = await getSubscriptionsForUser(userId);
if (!subscriptions || subscriptions.length === 0) {
return { success: false, reason: 'No subscriptions found' };
}
const payload = JSON.stringify({
title,
body,
data
});
const results = await Promise.all(
subscriptions.map(async (subscription) => {
try {
await webpush.sendNotification(subscription, payload);
return { success: true, subscription };
} catch (error) {
if (error.statusCode === 410) {
// Subscription has expired or is no longer valid
await removeSubscription(subscription);
}
return { success: false, subscription, error };
}
})
);
return {
success: true,
results
};
} catch (error) {
console.error('Error sending push notification:', error);
return { success: false, error };
}
}
// Enhance notification service to also send push notification
class PushNotificationService extends EnhancedNotificationService {
async sendToUser(userId, notificationType, payload) {
// Send socket.io notification
const socketResult = await super.sendToUser(userId, notificationType, payload);
// If no active connections, try to send push notification
if (socketResult === 0) {
// Check user preferences for push
const shouldPush = await shouldSendPushNotification(userId, notificationType);
if (shouldPush) {
// Format payload for push notification
const pushPayload = formatPushPayload(notificationType, payload);
// Send push notification
await sendPushNotification(
userId,
pushPayload.title,
pushPayload.body,
{
type: notificationType,
url: pushPayload.url,
...pushPayload.data
}
);
}
}
return socketResult;
}
}
// Helper to format push notification content
function formatPushPayload(type, payload) {
switch (type) {
case 'message':
return {
title: `New message from ${payload.sender}`,
body: payload.preview,
url: `/messages/${payload.conversationId}`,
data: { messageId: payload.messageId }
};
case 'friend-request':
return {
title: 'New Friend Request',
body: `${payload.name} wants to connect with you`,
url: '/friends/requests',
data: { requestId: payload.requestId }
};
// Add more notification types as needed
default:
return {
title: 'New Notification',
body: 'You have a new notification',
url: '/notifications',
data: {}
};
}
}
Offline Support and Syncing
Handle cases where users are offline and need to receive missed notifications when they reconnect.
// Enhanced notification service with offline support
class OfflineAwareNotificationService extends PushNotificationService {
constructor(io, db) {
super(io);
this.db = db; // Database connection for storing notifications
}
async sendToUser(userId, notificationType, payload) {
// Always store notification in database
await this.storeNotification(userId, notificationType, payload);
// Attempt to deliver via socket.io and push
return super.sendToUser(userId, notificationType, payload);
}
async storeNotification(userId, type, payload) {
try {
await this.db.collection('notifications').insertOne({
userId,
type,
payload,
read: false,
delivered: false,
createdAt: new Date()
});
} catch (error) {
console.error('Error storing notification:', error);
}
}
async markAsDelivered(notificationIds) {
try {
await this.db.collection('notifications').updateMany(
{ _id: { $in: notificationIds } },
{ $set: { delivered: true, deliveredAt: new Date() } }
);
} catch (error) {
console.error('Error marking notifications as delivered:', error);
}
}
async markAsRead(notificationIds) {
try {
await this.db.collection('notifications').updateMany(
{ _id: { $in: notificationIds } },
{ $set: { read: true, readAt: new Date() } }
);
} catch (error) {
console.error('Error marking notifications as read:', error);
}
}
async getUndeliveredNotifications(userId) {
try {
return await this.db.collection('notifications')
.find({ userId, delivered: false })
.sort({ createdAt: 1 })
.toArray();
} catch (error) {
console.error('Error getting undelivered notifications:', error);
return [];
}
}
}
// Socket.io handling for reconnection and notification syncing
io.on('connection', (socket) => {
// ... other connection handling
// Handle authentication and user registration
socket.on('auth', async (userData) => {
const userId = userData.userId;
// Register this socket with the user ID
notificationService.registerUser(userId, socket.id);
// Fetch undelivered notifications
const undeliveredNotifications = await notificationService.getUndeliveredNotifications(userId);
if (undeliveredNotifications.length > 0) {
// Send undelivered notifications to the client
socket.emit('notification-sync', undeliveredNotifications);
// Mark notifications as delivered
await notificationService.markAsDelivered(
undeliveredNotifications.map(notification => notification._id)
);
}
// Acknowledge successful authentication
socket.emit('auth_success', { userId });
});
// Handle notification read status updates
socket.on('mark-notifications-read', async (data) => {
const { notificationIds } = data;
await notificationService.markAsRead(notificationIds);
socket.emit('notifications-marked-read', { success: true, count: notificationIds.length });
});
});
Analytics and Insights
Track notification engagement to improve your notification strategy.
// Add analytics capabilities to notification service
class AnalyticsEnabledNotificationService extends OfflineAwareNotificationService {
constructor(io, db, analyticsService) {
super(io, db);
this.analyticsService = analyticsService;
}
async sendToUser(userId, notificationType, payload) {
// Track notification send event
this.analyticsService.trackEvent('notification_sent', {
userId,
notificationType,
timestamp: new Date()
});
return super.sendToUser(userId, notificationType, payload);
}
async markAsDelivered(notificationIds) {
// Get notifications before marking them as delivered
const notifications = await this.db.collection('notifications')
.find({ _id: { $in: notificationIds } })
.toArray();
// Track delivery events
for (const notification of notifications) {
this.analyticsService.trackEvent('notification_delivered', {
userId: notification.userId,
notificationId: notification._id.toString(),
notificationType: notification.type,
deliveryLatency: new Date() - notification.createdAt,
timestamp: new Date()
});
}
return super.markAsDelivered(notificationIds);
}
async markAsRead(notificationIds) {
// Get notifications before marking them as read
const notifications = await this.db.collection('notifications')
.find({ _id: { $in: notificationIds } })
.toArray();
// Track read events
for (const notification of notifications) {
const deliveredAt = notification.deliveredAt || notification.createdAt;
this.analyticsService.trackEvent('notification_read', {
userId: notification.userId,
notificationId: notification._id.toString(),
notificationType: notification.type,
readLatency: new Date() - deliveredAt,
totalLatency: new Date() - notification.createdAt,
timestamp: new Date()
});
}
return super.markAsRead(notificationIds);
}
// Get engagement metrics for notifications
async getEngagementMetrics(options = {}) {
const { startDate, endDate, notificationType } = options;
const query = {};
if (startDate || endDate) {
query.createdAt = {};
if (startDate) query.createdAt.$gte = new Date(startDate);
if (endDate) query.createdAt.$lte = new Date(endDate);
}
if (notificationType) {
query.type = notificationType;
}
try {
const totalCount = await this.db.collection('notifications')
.countDocuments(query);
const deliveredCount = await this.db.collection('notifications')
.countDocuments({ ...query, delivered: true });
const readCount = await this.db.collection('notifications')
.countDocuments({ ...query, read: true });
// Calculate average delivery and read times
const deliveryTimes = await this.db.collection('notifications')
.find({ ...query, delivered: true, deliveredAt: { $exists: true } })
.project({ deliveryTime: { $subtract: ['$deliveredAt', '$createdAt'] } })
.toArray();
const readTimes = await this.db.collection('notifications')
.find({ ...query, read: true, readAt: { $exists: true } })
.project({ readTime: { $subtract: ['$readAt', '$deliveredAt'] } })
.toArray();
const avgDeliveryTime = deliveryTimes.length > 0
? deliveryTimes.reduce((sum, item) => sum + item.deliveryTime, 0) / deliveryTimes.length
: null;
const avgReadTime = readTimes.length > 0
? readTimes.reduce((sum, item) => sum + item.readTime, 0) / readTimes.length
: null;
return {
totalCount,
deliveredCount,
readCount,
deliveryRate: totalCount > 0 ? deliveredCount / totalCount : 0,
readRate: deliveredCount > 0 ? readCount / deliveredCount : 0,
overallReadRate: totalCount > 0 ? readCount / totalCount : 0,
avgDeliveryTime,
avgReadTime
};
} catch (error) {
console.error('Error calculating engagement metrics:', error);
return null;
}
}
}
Practice Activities
Activity 1: Setting Up Basic Socket.io Notifications
Create a simple notification system using Socket.io that sends real-time alerts when events occur.
- Set up a basic Express server with Socket.io
- Create a simple front-end with a notification area
- Implement an API endpoint that triggers notifications
- Display toast notifications when events are received
Activity 2: Building a Notification Center
Expand your notification system to include a notification center that stores and displays a history of notifications.
- Create a database schema for storing notifications
- Implement APIs for fetching, marking as read, and deleting notifications
- Build a UI for the notification center with unread indicators
- Add filtering and sorting options for notifications
Activity 3: Implementing Notification Preferences
Add user preferences to your notification system to allow users to control what notifications they receive.
- Design a schema for notification preferences
- Create a UI for users to manage their preferences
- Modify your notification service to respect user preferences
- Implement a "Do Not Disturb" mode for notifications