civil-and-structural-engineering
Implementing Real-time Notifications in Javascript Web Apps
Table of Contents
Real-time notifications have become a cornerstone of modern web application design. Users expect instant feedback when events occur: a new chat message arrives, a colleague approves a document, or a server alert triggers. Building this functionality into a JavaScript web app requires careful planning, the right communication protocols, and thoughtful UI design. This guide walks you through implementing real-time notifications using WebSockets and Server-Sent Events (SSE), integrating them with backend systems like Directus, and ensuring a polished, production-ready user experience.
Why Real-Time Notifications Matter
Real-time notifications eliminate the need for manual page refreshes or polling, which drains bandwidth and degrades user experience. When a user receives an instant alert, they stay engaged and can react promptly. For SaaS platforms, collaboration tools, or e‑commerce dashboards, this immediacy directly impacts productivity and satisfaction. Studies show that reducing notification latency from seconds to milliseconds can boost user retention by over 20% (Nielsen Norman Group).
Beyond user experience, real-time notifications also enable new interaction patterns: live comment streams, collaborative editing cursors, and server‑side event feeds. Choosing the right transport layer is the first technical decision.
WebSocket: Full-Duplex Communication
WebSockets provide a persistent, bidirectional channel between a client and server. Unlike HTTP, the connection stays open after the initial handshake, allowing either side to push data at any time. This makes WebSockets ideal for chat applications, live financial feeds, and multiplayer games — any scenario where low‑latency, two‑way messaging is critical.
Establishing a WebSocket Connection in JavaScript
The browser’s native WebSocket API makes connection setup straightforward. You instantiate a new WebSocket object with the server’s URL (using the wss:// scheme for secure connections). Then attach event listeners for open, message, error, and close.
const socket = new WebSocket('wss://api.directus.app/websocket');
socket.onopen = () => {
console.log('WebSocket connection established.');
// Optionally send an authentication token
socket.send(JSON.stringify({ type: 'auth', token: 'your-jwt' }));
};
socket.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'notification') {
showToast(data.payload.message);
}
};
socket.onerror = (err) => {
console.error('WebSocket error:', err);
};
socket.onclose = (event) => {
console.warn('WebSocket closed:', event.code, event.reason);
// optional reconnect logic
};
After establishing the connection, you can send JSON‑formatted messages and handle responses in the onmessage handler. The server must support the WebSocket protocol; many Node.js frameworks (e.g., ws, Socket.IO) and CMS backends like Directus offer built‑in or plugin‑based WebSocket support.
Handling Reconnection and Heartbeats
A robust real-time system must handle network interruptions gracefully. Implement a reconnection strategy that backs off exponentially:
function connectWebSocket() {
const socket = new WebSocket('wss://api.directus.app/websocket');
let retryDelay = 1000;
socket.onclose = () => {
setTimeout(() => {
console.log('Reconnecting...');
connectWebSocket();
}, retryDelay);
retryDelay = Math.min(retryDelay * 2, 30000);
};
// ...other handlers
}
connectWebSocket();
Additionally, send periodic heartbeat pings (every 30–60 seconds) to detect stale connections. Many WebSocket libraries handle this automatically, but if you’re using the raw API, set an interval to send a { type: 'ping' } message and expect a pong response.
Server-Sent Events: Simpler, One-Way Streaming
Server-Sent Events (SSE) are a lightweight alternative when you only need the server to push data to clients without requiring client‑to‑server messages. SSE uses standard HTTP; the server responds with a Content-Type: text/event-stream header and keeps the connection open. The client reads the stream via the EventSource API.
SSE is simpler to implement than WebSockets, works over HTTP/2, and automatically reconnects when the connection drops. However, it does not support bidirectional communication, so it’s best suited for news feeds, stock tickers, or system alerts.
Using EventSource in the Browser
The browser’s EventSource interface is minimal:
const eventSource = new EventSource('/api/events?user_id=42');
eventSource.onopen = () => {
console.log('SSE connection opened.');
};
eventSource.addEventListener('notification', (event) => {
const data = JSON.parse(event.data);
displayNotification(data);
});
eventSource.onerror = (err) => {
console.error('EventSource error:', err);
// The browser will automatically attempt to reconnect
};
function displayNotification(data) {
// Update UI
}
On the server side, you format each event as text lines:
event: notification
data: {"message":"Your report is ready","severity":"info"}
Note that the EventSource API only supports GET requests and cannot send custom headers. If you need to pass authentication tokens, append them as query parameters (use HTTPS to avoid exposing the token).
WebSocket vs. SSE: Choosing the Right Approach
Both technologies are capable of handling real-time notifications, but they serve different use cases:
| Feature | WebSocket | SSE |
|---|---|---|
| Direction | Bidirectional | Server → Client only |
| Auto‑reconnect | Must implement manually | Built‑in |
| Binary data | Yes (ArrayBuffer, Blob) | Text only (UTF‑8) |
| Browser support | Excellent (IE10+) | Good (no IE/Edge Legacy) |
| Complexity | Higher | Lower |
If the client needs to send commands or data back to the server (e.g., marking a notification as read), WebSocket is the natural choice. For simple notification feeds, SSE reduces development overhead and is easier to debug because it uses standard HTTP headers.
Integrating Real-Time Notifications with Directus
Directus is a headless CMS that exposes a REST and GraphQL API. To add real-time capabilities, you can leverage its WebSocket support (introduced in Directus 10.x) or set up an SSE endpoint via a custom extension. The WebSocket API allows you to subscribe to changes on specific collections, making it straightforward to push notifications when a new record is created or updated.
WebSocket Subscription in Directus
Directus’s WebSocket endpoint (wss://your-directus-instance/websocket) accepts JSON messages to subscribe, unsubscribe, or authenticate. For example, to listen for new messages in a notifications collection:
const ws = new WebSocket('wss://cms.example.com/websocket');
ws.onopen = () => {
// Subscribe to changes on the notifications collection
ws.send(JSON.stringify({
type: 'subscribe',
collection: 'notifications',
query: { filter: { user_id: { _eq: currentUserId } } }
}));
};
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.type === 'subscription' && msg.event === 'create') {
showNotification(msg.data);
}
};
This approach offloads the complexity of real-time synchronization to the CMS, while your JavaScript app only needs to handle the incoming data. For authentication, send a token in the first WebSocket message (as shown earlier).
Displaying Notifications in the UI
Once you have the data, the user experience depends on how you present it. Styled toast notifications are the most common pattern: a small, non‑intrusive popup that appears at the corner of the screen and auto‑dismisses after a few seconds. Libraries like Toastr, Notyf, or Sonner provide ready‑to‑use components with accessible markup and animation.
Alternatively, build a custom notification component. Here’s a minimal example using vanilla JavaScript and CSS:
function showToast(message, type = 'info') {
const toast = document.createElement('div');
toast.className = `toast toast-${type}`;
toast.textContent = message;
toast.setAttribute('role', 'alert');
document.getElementById('toast-container').appendChild(toast);
setTimeout(() => toast.remove(), 3000);
}
// CSS (simplified):
.toast {
padding: 12px 20px;
margin-bottom: 8px;
border-radius: 4px;
color: #fff;
opacity: 0.9;
transition: opacity 0.3s;
}
.toast-info { background: #007bff; }
.toast-error { background: #dc3545; }
.toast-success { background: #28a745; }
Make sure the container is fixed to the top‑right corner and that toasts stack vertically. Use aria-live="polite" on the container to ensure screen readers announce new notifications.
User Preferences and Notification Management
Not all notifications are equally important. Allow users to customize which events they want to receive and through which channels (in‑app, email, push). Store preferences in the backend and filter events on the server before pushing them to the client. For example, a user might disable “new comment” alerts but keep “task assigned” alerts.
In your JavaScript app, periodically fetch the user’s preference profile and adjust the subscription filters accordingly. If using Directus WebSockets, you can send an updated subscription query when preferences change.
Fallback for Unsupported Browsers
While modern browsers broadly support WebSockets and SSE, older environments (e.g., Internet Explorer 11 for WebSockets, IE/Edge Legacy for SSE) may require polyfills or fallback strategies. A common approach:
- Detect support with
typeof WebSocket !== 'undefined'ortypeof EventSource !== 'undefined'. - Polling fallback: Use a long‑polling endpoint that returns new events as JSON. The client requests the endpoint every few seconds and processes any pending events.
- Library abstraction: Use a library like
Socket.IOthat transparently falls back from WebSocket to HTTP long‑polling.
When polling, set a reasonable interval (e.g., 5‑10 seconds) and return an empty response if no events are pending. To reduce load, use If-Modified-Since headers or timestamp‑based queries.
Security Considerations
Real-time connections introduce several security vectors that must be addressed:
- Authenticate every connection. For WebSockets, send a JWT or session token in the first message. For SSE, append a token as a query parameter (but never in the URL if you log it).
- Validate and sanitize all data before pushing it to the client. Even if your backend is trusted, never output raw user‑generated content into a notification without escaping.
- Use secure protocols (
wss://,https://) to prevent man‑in‑the‑middle attacks. - Rate‑limit connections per user to prevent abuse. Directus exposes rate‑limiting settings that you can configure.
- Do not expose internal server state through WebSocket or SSE messages. Always return only the data the user is authorized to see.
Performance and Scalability
As the number of concurrent connections grows, your server must efficiently manage them. Consider these optimizations:
- Use a dedicated WebSocket server (e.g., separate Node.js process) and scale horizontally with a load balancer that supports WebSocket sticky sessions.
- Broadcast only to relevant users. Use rooms or channels based on user ID, group, or subscription filter to avoid sending every event to every client.
- Compress messages. For text‑based protocols, enable per‑message deflate (WebSocket) or gzip (SSE over HTTP/2).
- Monitor connection health with metrics like open connections, message throughput, and error rates. Tools like Prometheus can scrape these from your server.
- Consider server‑sent events caching. With SSE, you can leverage HTTP caching headers if the stream is static for a period (though notifications are usually dynamic).
Putting It All Together: A Complete Workflow
To illustrate a real‑world example, let’s combine a Directus backend with a JavaScript frontend that uses SSE for notifications.
- Backend (Directus Extension): Create a custom endpoint at
/api/eventsthat checks the user’s JWT token, then streams new notifications from a queue (e.g., Redis pub/sub or a Directus hook that writes to anotificationscollection). The endpoint returnsContent-Type: text/event-stream. - Frontend: Upon page load, authenticate with Directus and open an
EventSourcepointing to/api/events?token=.... Listen fornotificationevents. - Display: Each received notification is rendered as a toast using a custom component, with options to dismiss or open the related resource.
- User Preferences: When the user updates their notification settings via a form, send a POST to Directus’s
/users/meendpoint. The backend refreshes the event stream filter for that session.
This architecture keeps the client lean and pushes the heavy lifting to Directus and its hook system.
Testing Real-Time Notifications
Before deploying, thoroughly test your implementation:
- Load testing: Use tools like Artillery to simulate hundreds of concurrent WebSocket or SSE connections. Measure latency and connection stability.
- Network throttling: Use Chrome DevTools to simulate slow 3G or offline conditions. Verify that reconnection logic works and that no duplicate notifications are delivered.
- Cross‑browser testing: Test in Firefox, Safari, Chrome, and Edge. For SSE, test in Safari (which lacks full EventSource support for custom events – you may need to use a polyfill).
- Security edge cases: Attempt to connect with expired tokens, or inject malformed JSON to ensure your error handlers don’t crash the client.
Conclusion
Real-time notifications are no longer a luxury; they are a baseline expectation in modern web applications. By leveraging WebSockets or SSE, combined with a backend like Directus that provides real‑time hooks, you can deliver instant updates without overwhelming your infrastructure. Focus on user customization, graceful fallbacks, and robust security to create a notification system that feels seamless and reliable.
Start small: implement a simple toast for one event type, then gradually expand to multiple channels and user preferences. The key is to iterate on the user experience while keeping the underlying transport efficient and maintainable. With the approaches outlined here, you’ll be well on your way to a production‑ready real‑time feature.