Implémentez un système de notifications et chat temps réel avec Angular frontend et Node.js/Socket.io backend : WebSocket, events, authentification JWT, scalabilité Redis.
1. Concepts WebSocket et Socket.io
WebSocket est un protocole standardisé (RFC 6455) qui établit une connexion bidirectionnelle persistante entre un client et un serveur via une unique connexion TCP. Contrairement à HTTP qui suit un modèle request/response, WebSocket permet au serveur d'envoyer des données au client sans que celui-ci ne le demande. Cela ouvre des possibilités pour les applications temps réel : notifications instantanées, chat live, collaboration en temps réel, dashboards actualisés dynamiquement.
Socket.io est une bibliothèque Node.js qui abstrait les complexités de WebSocket et ajoute des fonctionnalités essentielles en production :
- Fallback automatique : si WebSocket échoue, Socket.io bascule sur polling long (HTTP long-polling)
- Reconnexion automatique : le client se reconnecte en cas de déconnexion
- Namespaces et rooms : organiser les connexions (ex : une room par chat, par notification type)
- Émission d'événements : pattern event-based au lieu de raw frames
- Scalabilité : avec Redis adapter, synchroniser plusieurs instances serveur
- Middleware : authentification, authorization au niveau Socket
2. Installation et setup Express + Socket.io
Commençons par un projet Node.js minimal avec Express et Socket.io :
# Créer le projet
mkdir socketio-app && cd socketio-app
npm init -y
# Installer dépendances
npm install express socket.io cors dotenv
npm install --save-dev nodemon
Créer le fichier server.js :
// server.js - Serveur Express + Socket.io minimal
const express = require('express');
const { createServer } = require('http');
const { Server } = require('socket.io');
const cors = require('cors');
require('dotenv').config();
const app = express();
const httpServer = createServer(app);
// Configuration Socket.io avec CORS (Angular client)
const io = new Server(httpServer, {
cors: {
origin: process.env.CLIENT_URL || 'http://localhost:4200', // Autoriser Angular local
credentials: true, // Cookies/auth headers
},
});
// Middlewares Express
app.use(cors());
app.use(express.json());
// Route santé
app.get('/api/health', (req, res) => {
res.json({ status: 'OK', timestamp: new Date().toISOString() });
});
// SOCKET.IO - Gestion événements temps réel
io.on('connection', (socket) => {
console.log(`✓ Client connecté: ${socket.id}`);
// Événement: client envoie un message
socket.on('message:send', (data) => {
console.log(`Message reçu de ${socket.id}:`, data);
// Diffuser à TOUS les clients
io.emit('message:received', {
from: socket.id,
text: data.text,
timestamp: new Date(),
});
});
// Événement: client se déconnecte
socket.on('disconnect', () => {
console.log(`✗ Client déconnecté: ${socket.id}`);
});
});
// Démarrer serveur
const PORT = process.env.PORT || 3000;
httpServer.listen(PORT, () => {
console.log(`🚀 Serveur Socket.io lancé sur http://localhost:${PORT}`);
});
Ajouter au package.json :
{
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js"
}
}
Tester le serveur :
# Lancer en mode dev (hot-reload avec nodemon)
npm run dev
# Output: 🚀 Serveur Socket.io lancé sur http://localhost:3000
3. Architecture du serveur Node.js
Pour une application production, il faut structurer le code en modules. Voici une architecture recommandée :
socketio-app/
├── server.js # Point d'entrée
├── config/
│ └── socket.config.js # Configuration Socket.io
├── events/
│ ├── message.js # Handlers événements messages
│ ├── notification.js # Handlers notifications
│ └── index.js # Agréger tous les events
├── middleware/
│ └── auth.js # Middleware authentification
├── utils/
│ └── logger.js # Logging
├── .env
└── package.json
Exemple : config/socket.config.js
// config/socket.config.js - Configuration centralisée Socket.io
module.exports = {
// Options de reconnexion côté client
reconnection: true,
reconnectionDelay: 1000, // Délai initial (ms)
reconnectionDelayMax: 5000, // Délai max (ms)
reconnectionAttempts: Infinity, // Tentatives infinies
// CORS pour Angular
cors: {
origin: process.env.CLIENT_URL || 'http://localhost:4200',
credentials: true,
},
// Namespaces pour organiser les connexions
namespaces: {
'/': 'Default namespace',
'/notifications': 'Notifications utilisateur',
'/chat': 'Chat temps réel',
},
};
Exemple : events/message.js
// events/message.js - Gérer événements messages
module.exports = (io, socket) => {
// Événement: envoyer message à une room spécifique
socket.on('message:send', (data, callback) => {
const { roomId, text, userId } = data;
// Validation côté serveur (JAMAIS faire confiance au client)
if (!text || text.trim().length === 0) {
callback({ error: 'Message vide' });
return;
}
// Construire le message avec métadonnées serveur
const message = {
id: Date.now(), // Générateur d'ID simple (utiliser UUID en prod)
userId,
text: text.trim(),
timestamp: new Date(),
socketId: socket.id,
};
// Envoyer UNIQUEMENT à la room spécifique
io.to(roomId).emit('message:new', message);
// Callback côté client pour confirmation
callback({ success: true, messageId: message.id });
});
// Rejoindre une room
socket.on('room:join', (data) => {
const { roomId } = data;
socket.join(roomId); // Socket.io built-in method
console.log(`${socket.id} joined room ${roomId}`);
// Notifier les autres dans la room
io.to(roomId).emit('room:user-joined', {
userId: data.userId,
socketId: socket.id,
totalUsers: io.sockets.adapter.rooms.get(roomId)?.size || 1,
});
});
// Quitter une room
socket.on('room:leave', (data) => {
const { roomId } = data;
socket.leave(roomId);
io.to(roomId).emit('room:user-left', {
socketId: socket.id,
totalUsers: io.sockets.adapter.rooms.get(roomId)?.size || 0,
});
});
};
4. Intégration Angular et Socket.io Client
Côté Angular, créer un service pour gérer la connexion Socket.io :
# Générer le service
ng generate service services/socket
Implémenter services/socket.service.ts :
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { io, Socket } from 'socket.io-client';
import { environment } from '../../environments/environment';
interface SocketMessage {
id: string | number;
userId: string;
text: string;
timestamp: Date;
socketId: string;
}
@Injectable({
providedIn: 'root'
})
export class SocketService {
private socket: Socket | null = null;
private messageSubject = new BehaviorSubject<SocketMessage[]>([]);
private connectionStatusSubject = new BehaviorSubject<boolean>(false);
public messages$ = this.messageSubject.asObservable();
public connected$ = this.connectionStatusSubject.asObservable();
constructor() {}
// Établir la connexion Socket.io
connect(token: string): void {
if (this.socket?.connected) {
console.log('✓ Socket déjà connecté');
return;
}
// Créer socket avec authentification (token JWT)
this.socket = io(environment.apiUrl, {
auth: {
token: token, // Envoyer token pour authentification serveur
},
reconnection: true,
reconnectionDelay: 1000,
reconnectionDelayMax: 5000,
});
// Événement: connexion établie
this.socket.on('connect', () => {
console.log('✓ Socket connecté:', this.socket?.id);
this.connectionStatusSubject.next(true);
});
// Événement: nouveau message reçu
this.socket.on('message:new', (message) => {
const currentMessages = this.messageSubject.value;
this.messageSubject.next([...currentMessages, message]);
});
// Événement: utilisateur rejoint la room
this.socket.on('room:user-joined', (data) => {
console.log(`${data.userId} a rejoint (total: ${data.totalUsers})`);
});
// Événement: déconnexion
this.socket.on('disconnect', () => {
console.log('✗ Socket déconnecté');
this.connectionStatusSubject.next(false);
});
// Gestion erreurs
this.socket.on('error', (error) => {
console.error('Socket erreur:', error);
});
}
// Envoyer un message avec timeout
sendMessage(roomId: string, text: string, timeout = 5000): Promise<any> {
return new Promise((resolve, reject) => {
if (!this.socket?.connected) {
reject(new Error('Socket non connecté'));
return;
}
// Créer un timer pour éviter les envois sans réponse
const timer = setTimeout(() => {
reject(new Error(`Timeout: serveur n'a pas répondu après ${timeout}ms`));
}, timeout);
// Émettre l'événement avec callback
this.socket.emit('message:send', { roomId, text }, (response: any) => {
clearTimeout(timer); // Annuler le timeout si réponse reçue
if (response?.error) {
reject(new Error(response.error));
} else {
resolve(response);
}
});
});
}
// Rejoindre une room
joinRoom(roomId: string, userId: string): void {
this.socket?.emit('room:join', { roomId, userId });
}
// Quitter une room
leaveRoom(roomId: string): void {
this.socket?.emit('room:leave', { roomId });
}
// Écouter un événement custom (avec cleanup pour éviter memory leaks)
on(eventName: string): Observable<any> {
return new Observable((subscriber) => {
const handler = (data: any) => subscriber.next(data);
this.socket?.on(eventName, handler);
// Cleanup: supprimer le listener quand l'observable est unsubscribed
return () => {
this.socket?.off(eventName, handler);
};
});
}
// Déconnecter
disconnect(): void {
this.socket?.disconnect();
this.connectionStatusSubject.next(false);
}
}
Utiliser le service dans un composant :
import { Component, OnInit, OnDestroy } from '@angular/core';
import { SocketService } from './services/socket.service';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@Component({
selector: 'app-chat',
templateUrl: './chat.component.html',
styleUrls: ['./chat.component.css']
})
export class ChatComponent implements OnInit, OnDestroy {
messages: any[] = [];
messageInput: string = '';
connected: boolean = false;
roomId: string = 'general'; // Room par défaut
destroy$ = new Subject<void>();
constructor(
private socketService: SocketService,
private authService: AuthService // Supposé fournir le JWT token
) {}
ngOnInit(): void {
// Récupérer le token et connecter
const token = this.authService.getToken();
this.socketService.connect(token);
// S'abonner aux messages
this.socketService.messages$
.pipe(takeUntil(this.destroy$))
.subscribe((messages) => {
this.messages = messages;
});
// S'abonner au statut de connexion
this.socketService.connected$
.pipe(takeUntil(this.destroy$))
.subscribe((connected) => {
this.connected = connected;
});
// Rejoindre la room
const userId = this.authService.getUserId();
this.socketService.joinRoom(this.roomId, userId);
}
// Envoyer un message
async sendMessage(): Promise<void> {
if (!this.messageInput.trim()) return;
try {
await this.socketService.sendMessage(this.roomId, this.messageInput);
this.messageInput = ''; // Vider le champ
} catch (error) {
console.error('Erreur envoi:', error);
}
}
ngOnDestroy(): void {
this.socketService.leaveRoom(this.roomId);
this.socketService.disconnect();
this.destroy$.next();
}
}
5. Cas d'usage : notifications et chat
Cas 1: Notifications en temps réel
Une notification doit être envoyée UNIQUEMENT à l'utilisateur destinataire. Utiliser les rooms privées ou un namespace dédié :
// Server: events/notification.js
module.exports = (io, socket) => {
// Rejoindre une room privée à l'authentification
socket.on('user:ready', (data) => {
const { userId } = data;
// Créer une room privée par utilisateur
socket.join(`user:${userId}`);
console.log(`${socket.id} joined private room user:${userId}`);
});
// Admin envoie notification à un utilisateur
socket.on('notification:send', (data) => {
const { userId, title, message } = data;
// Envoyer UNIQUEMENT à l'utilisateur (room privée)
io.to(`user:${userId}`).emit('notification:received', {
id: Date.now(),
title,
message,
timestamp: new Date(),
});
});
// Broadcast notification à TOUS les utilisateurs
socket.on('notification:broadcast', (data) => {
io.emit('notification:all', {
title: data.title,
message: data.message,
timestamp: new Date(),
});
});
};
Cas 2: Chat en temps réel
// Angular: notification.service.ts
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { SocketService } from './socket.service';
@Injectable({
providedIn: 'root'
})
export class NotificationService {
private notificationsSubject = new BehaviorSubject<any[]>([]);
public notifications$ = this.notificationsSubject.asObservable();
constructor(private socketService: SocketService) {
// Écouter les notifications
this.socketService.on('notification:received').subscribe((notification) => {
const current = this.notificationsSubject.value;
this.notificationsSubject.next([...current, notification]);
// Auto-remove après 5s
setTimeout(() => {
const updated = this.notificationsSubject.value.filter(
(n) => n.id !== notification.id
);
this.notificationsSubject.next(updated);
}, 5000);
});
}
}
6. Authentification et sécurité WebSocket
Socket.io doit authentifier les clients avec un token JWT avant d'autoriser les connexions sensibles.
// Server: middleware/auth.js
const jwt = require('jsonwebtoken');
function socketAuthMiddleware(socket, next) {
// Récupérer le token du handshake (auth headers)
const token = socket.handshake.auth.token;
if (!token) {
return next(new Error('Authentification requise'));
}
try {
// Vérifier et décoder le JWT
const decoded = jwt.verify(token, process.env.JWT_SECRET);
// Attacher l'utilisateur au socket pour accès ultérieur
socket.userId = decoded.id;
socket.username = decoded.username;
socket.roles = decoded.roles || [];
next(); // Continuer
} catch (error) {
return next(new Error('Token invalide'));
}
}
module.exports = socketAuthMiddleware;
Appliquer le middleware au serveur :
// server.js
const socketAuthMiddleware = require('./middleware/auth');
// Appliquer avant connection handler
io.use(socketAuthMiddleware);
io.on('connection', (socket) => {
// socket.userId et socket.username sont maintenant disponibles
console.log(`✓ ${socket.username} (${socket.userId}) connecté`);
});
Bonnes pratiques sécurité :
- ✅ Valider TOUS les inputs côté serveur (jamais faire confiance au client)
- ✅ Vérifier les autorisations avant d'émettre (l'utilisateur a-t-il accès à cette room ?)
- ✅ Utiliser rate limiting pour éviter les abus
- ✅ Loger les connexions/déconnexions pour audit
- ✅ Masquer les IDs internes (utiliser nanoid au lieu de Date.now())
7. Gestion des reconnexions et état
Socket.io gère automatiquement les reconnexions, mais il faut gérer l'état applicatif pour éviter les messages perdus.
// Angular: message.service.ts - Gérer l'état avec synchronisation
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { SocketService } from './socket.service';
import { HttpClient } from '@angular/common/http';
@Injectable({
providedIn: 'root'
})
export class MessageService {
private messagesSubject = new BehaviorSubject<any[]>([]);
public messages$ = this.messagesSubject.asObservable();
private lastSyncTimestamp = 0;
constructor(
private socketService: SocketService,
private http: HttpClient
) {
// Quand reconnexion, synchroniser les messages
this.socketService.connected$.subscribe((connected) => {
if (connected) {
this.syncMessages(); // Récupérer les nouveaux messages depuis le serveur
}
});
}
// Récupérer les messages perdus pendant la déconnexion
private syncMessages(): void {
const query = `since=${this.lastSyncTimestamp}`;
this.http.get<any[]>(`/api/messages?${query}`).subscribe((messages) => {
const current = this.messagesSubject.value;
this.messagesSubject.next([...current, ...messages]);
// Mettre à jour timestamp pour la prochaine sync
if (messages.length > 0) {
this.lastSyncTimestamp = messages[messages.length - 1].timestamp;
}
});
}
addMessage(message: any): void {
const current = this.messagesSubject.value;
this.messagesSubject.next([...current, message]);
this.lastSyncTimestamp = message.timestamp;
}
}
Pattern: Acknowledgment callbacks pour garantir la livraison (version moderne avec async/await) :
// Server - Pattern moderne avec async/await (meilleur que callbacks)
socket.on('message:send', async (data, ack) => {
try {
// Valider les données
if (!data.text || !data.roomId) {
ack({ error: 'Données invalides' });
return;
}
// Sauvegarder en DB (promise-based)
const result = await saveMessageToDatabase({
...data,
timestamp: new Date(),
});
// Diffuser aux autres clients dans la room
io.to(data.roomId).emit('message:new', result);
// Confirmer au client (acknowledgment)
ack({ success: true, id: result.id, timestamp: result.timestamp });
} catch (error) {
console.error('Erreur message:send:', error.message);
ack({ error: error.message || 'Erreur serveur' });
}
});
// Client Angular
await this.socketService.sendMessage(roomId, text);
// Ne continue que si serveur a confirmé
8. Scalabilité avec Redis adapter
En production, vous aurez multiple instances Node.js derrière un load balancer. Socket.io doit synchroniser les événements via Redis.
# Installer Redis adapter
npm install socket.io-redis redis
// server.js - Avec Redis adapter
const express = require('express');
const { createServer } = require('http');
const { Server } = require('socket.io');
const { createAdapter } = require('@socket.io/redis-adapter');
const { createClient } = require('redis');
const app = express();
const httpServer = createServer(app);
// Créer clients Redis (pub/sub)
const pubClient = createClient({ host: 'localhost', port: 6379 });
const subClient = pubClient.duplicate();
// Attendre la connexion Redis
Promise.all([pubClient.connect(), subClient.connect()]).then(() => {
// Configurer Socket.io avec Redis adapter
const io = new Server(httpServer, {
adapter: createAdapter(pubClient, subClient),
cors: { origin: 'http://localhost:4200' },
});
io.on('connection', (socket) => {
console.log(`${socket.id} connecté`);
socket.on('message:send', (data, ack) => {
// Émettre à TOUS les clients sur TOUTES les instances
// Redis synchronise automatiquement
io.to(data.roomId).emit('message:new', {
...data,
timestamp: new Date(),
});
ack({ success: true });
});
});
const PORT = process.env.PORT || 3000;
httpServer.listen(PORT, () => {
console.log(`🚀 Socket.io (instance) sur port ${PORT}`);
});
});
Architecture production :
┌─────────────┐
│ Angular │ (Client)
└──────┬──────┘
│ WebSocket
┌────▼────────────────┐
│ Load Balancer │ (Nginx / HAProxy)
└────┬───────┬────┬───┘
│ │ │
┌───▼──┐┌──▼──┐┌─▼───┐
│Node1 ││Node2 ││Node3 │ (Socket.io instances)
└───┬──┘└──┬──┘└─┬───┘
│ │ │
┌───▼──────▼─────▼────┐
│ Redis Cluster │ (Pub/Sub synchronization)
└────────────────────┘
9. Tests et debugging
Tester Socket.io correctement est crucial pour éviter les bugs en production. Voici comment tester les événements, les reconnexions et les edge cases.
Tests unitaires côté serveur :
# Installer les dépendances de test
npm install --save-dev jest socket.io-client-mock
// __tests__/socket.test.js - Tester les événements Socket.io
const { Server } = require('socket.io');
const { createServer } = require('http');
const Client = require('socket.io-client');
describe('Socket.io Events', () => {
let io, serverSocket, clientSocket;
beforeAll((done) => {
const httpServer = createServer();
io = new Server(httpServer, { cors: { origin: '*' } });
httpServer.listen(() => {
const port = httpServer.address().port;
clientSocket = Client(`http://localhost:${port}`, {
reconnection: false
});
io.on('connection', (socket) => {
serverSocket = socket;
});
clientSocket.on('connect', done);
});
});
afterAll(() => {
io.close();
clientSocket.close();
});
// Test 1: Envoyer et recevoir un message
test('should emit and receive message', (done) => {
serverSocket.on('message:send', (data) => {
expect(data.text).toBe('Hello');
done();
});
clientSocket.emit('message:send', { text: 'Hello' });
});
// Test 2: Acknowledment callbacks
test('should handle acknowledgment', (done) => {
serverSocket.on('message:send', (data, ack) => {
ack({ success: true, id: 123 });
});
clientSocket.emit('message:send', { text: 'Hi' }, (response) => {
expect(response.success).toBe(true);
done();
});
});
// Test 3: Room joins/leaves
test('should join and leave room', (done) => {
let joinCount = 0;
serverSocket.on('room:join', (data) => {
joinCount++;
expect(data.roomId).toBe('chat-123');
serverSocket.on('room:leave', (data) => {
expect(data.roomId).toBe('chat-123');
expect(joinCount).toBe(1);
done();
});
clientSocket.emit('room:leave', { roomId: 'chat-123' });
});
clientSocket.emit('room:join', { roomId: 'chat-123', userId: 'user1' });
});
});
Tests côté Angular :
// socket.service.spec.ts - Tester le service Socket.io Angular
import { TestBed } from '@angular/core/testing';
import { SocketService } from './socket.service';
describe('SocketService', () => {
let service: SocketService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(SocketService);
});
test('should create', () => {
expect(service).toBeTruthy();
});
// Test: connexion établie
test('should set connected status on connect', (done) => {
service.connected$.subscribe((connected) => {
if (connected) {
expect(connected).toBe(true);
service.disconnect();
done();
}
});
// Mock token
service.connect('mock-token-123');
});
// Test: envoi de message avec timeout
test('should reject if socket timeout', (done) => {
service.sendMessage('room-1', 'test', 100).catch((error) => {
expect(error.message).toContain('Timeout');
done();
});
});
});
Debugging : Outils essentiels
- Socket.io DevTools (Chrome) : visualiser tous les événements émis/reçus
- Console Node.js : ajouter logs avec `console.log()` et `console.error()`
- Network tab (DevTools) : inspecter les handshakes WebSocket
- Angular DevTools : monitorer les Observables et les changements de state
// Logger centralisé - events/logger.js
module.exports = (io, socket) => {
// Logger tous les événements reçus
const originalOnAny = socket.onAny;
socket.onAny((event, ...args) => {
console.log(`[Socket ${socket.id}] Event: ${event}`, args);
});
// Logger les erreurs
socket.on('error', (error) => {
console.error(`[Socket ${socket.id}] Error:`, error);
});
socket.on('disconnect', (reason) => {
console.log(`[Socket ${socket.id}] Disconnected:`, reason);
});
};
10. Exercices pratiques
Appliquez vos connaissances avec ces 3 exercices progressifs. Chaque exercice complète le précédent.
Exercice 1 : Chat simple (Débutant, ~15 min)
Objectif : Créer un chat minimal où 2 utilisateurs échangent des messages en temps réel.
- Créer serveur Express + Socket.io sur port 3000
- Créer client HTML simple avec champ de texte
- Émettre 'message:send' quand utilisateur écrit
- Afficher les messages reçus dans une div
- Tester : ouvrir 2 onglets et converser
Livrable : Capture d'écran montrant 2 messages (un de chaque côté) synchronisés en temps réel.
Exercice 2 : Chat multi-rooms (Intermédiaire, ~25 min)
Objectif : Ajouter des rooms (#general, #random, #tech) avec compteur d'utilisateurs.
- Créer 3 boutons pour sélectionner une room
- Implémenter 'room:join' et 'room:leave'
- Afficher le nombre d'utilisateurs par room (via 'room:user-joined')
- Les messages doivent être isolés par room (io.to(roomId).emit())
- Tester : 3 onglets, 2 en #general, 1 en #random
Livrable : Vidéo courte (30s) montrant switch entre rooms et compteur qui change.
Exercice 3 : Chat persistant + Reconnexions (Avancé, ~40 min)
Objectif : Implémenter persistance DB et gestion des reconnexions sans perdre de messages.
- Ajouter PostgreSQL (ou SQLite) pour persister les messages
- Au join d'une room, charger l'historique des 50 derniers messages
- Implémenter la synchronisation : si déconnexion, requête GET /api/messages?since=timestamp
- Tester déconnexion : arrêter le serveur 3s, redémarrer, vérifier que messages manquants sont refetchés
- Bonus : ajouter "typing..." indicator quand utilisateur tape
Livrable : Historique de 100+ messages chargé en <1s, reconnexion automatique fonctionnelle.
E1 = temps réel basique | E2 = multi-rooms | E3 = scalable production ✅
Conclusion
Socket.io simplifie considérablement l'implémentation de fonctionnalités temps réel en production. Grâce à sa gestion automatique des fallbacks, reconnexions et de la scalabilité via Redis, vous pouvez construire des applications robustes et responsives.
📋 Checklist avant production
- ✅ Authentification : tous les sockets doivent authentifier avec JWT
- ✅ Validation : vérifier les données côté serveur (jamais trust client)
- ✅ Rate limiting : limiter le nombre d'événements par client/seconde
- ✅ Logging : logger les connexions, erreurs et événements sensibles
- ✅ Tests : tester les reconnexions, room joins/leaves, edge cases
- ✅ Redis adapter : configurer pour scalabilité horizontale
- ✅ CORS : restreindre aux domaines autorisés
- ✅ Monitoring : surveiller la latence, nombre de connexions, erreurs