WebSocket temps reel avec Claude Code : auth, reconnexion, heartbeat et rooms
Construisez un WebSocket fiable avec Claude Code : authentification, reconnexion, heartbeat, rooms, validation et revue.
WebSocket est utile quand l’interface doit changer des que le serveur connait une nouvelle information : une tache Claude Code passe en phase de test, un commentaire arrive dans une revue, un utilisateur rejoint une room, ou une alerte de production doit apparaitre tout de suite. Claude Code peut generer un premier jet rapidement, mais une fonctionnalite temps reel publiable demande plus qu’un onmessage qui marche.
Les problemes arrivent souvent au meme endroit : authentification, tempete de reconnexions, connexions mobiles mortes, JSON invalide, consommateurs lents, fuite entre rooms et absence de journal d’audit. Cet article fournit un serveur Node.js avec ws, un client navigateur avec backoff, une validation de schema, un heartbeat, un client de test et un prompt de revue Claude Code.
Pour les references officielles, gardez sous la main MDN WebSocket, readyState et close. Pour la discipline de travail avec Claude Code, consultez Claude Code common workflows.
Quand choisir WebSocket
WebSocket est un canal bidirectionnel et durable. Il ne remplace pas les API HTTP, les files, les ecritures en base, la facturation ni les journaux d’audit. Utilisez-le pour la coordination rapide ; gardez les actions critiques dans des chemins serveur durables.
| Option | Ideal pour | A eviter pour | Instruction a donner a Claude Code |
|---|---|---|---|
| WebSocket | Chat, interface collaborative, progression live, broadcast par room | Notifications rares, historique consultable | Inclure auth, reconnexion, heartbeat et backpressure |
| Server-Sent Events | Evenements serveur vers navigateur | Actions frequentes du navigateur vers le serveur | Definir IDs et types d’evenements |
| Polling | Admin qui rafraichit toutes les 15-60 secondes | UX a faible latence | Ajouter cache et rate limit |
| HTTP API | Sauvegarde, paiement, audit, operations admin | Typing indicators ou presence tres frequente | Reutiliser les controles d’autorisation |
Trois cas d’usage concrets : afficher la progression d’une longue tache Claude Code, synchroniser commentaires et etats de lecture dans une room de revue, et diffuser des alertes operationnelles par client ou equipe. En formation, le meme modele permet au formateur de voir l’avancement de chaque participant en temps reel.
Architecture
flowchart LR
Browser["Browser client"] -->|"auth message + join room"| Server["Node ws server"]
Server --> Validator["JSON schema validator"]
Server --> Rooms["room registry"]
Server --> Audit["audit log / database"]
Rooms -->|"broadcast"| Browser
Server -->|"ping"| Browser
Browser -->|"auto pong / reconnect"| Server
Claude["Claude Code review loop"] --> Server
Le backpressure designe le moment ou le recepteur ne suit plus et ou le buffer d’envoi grossit. MDN indique que l’API WebSocket classique ne le gere pas automatiquement ; l’implementation doit donc surveiller bufferedAmount.
Demander a Claude Code les cas d’echec
Ne demandez pas seulement “un chat temps reel”. Donnez les contraintes des le depart.
Create a Node.js ws realtime WebSocket foundation.
Requirements:
- Browser sends an auth message after connect; join/chat are forbidden before auth
- Clients join by roomId and broadcasts only go to the same room
- Validate JSON messages by type and close invalid messages with 1008
- Implement heartbeat with server ping and pong tracking
- Close slow consumers with 1013 when bufferedAmount is too high
- Browser client uses exponential backoff with jitter
- Do not put secrets in URL query strings
- Do not treat WebSocket delivery as an audit log replacement
Serveur Node ws executable
Lancez-le en local :
npm init -y
npm install ws
node server.mjs
Creez server.mjs. En production, remplacez DEV_TOKEN par un ticket court, un cookie de session, ou une identite deja verifiee par votre gateway.
import { createServer } from "node:http";
import { randomUUID } from "node:crypto";
import { WebSocket, WebSocketServer } from "ws";
const PORT = Number(process.env.PORT || 8080);
const DEV_TOKEN = process.env.DEV_TOKEN || "dev-token";
const AUTH_TIMEOUT_MS = 5000;
const HEARTBEAT_INTERVAL_MS = 30000;
const MAX_BUFFERED_BYTES = 1024 * 1024;
const rooms = new Map();
const server = createServer((req, res) => {
res.writeHead(200, { "content-type": "text/plain" });
res.end("WebSocket server is running\n");
});
const wss = new WebSocketServer({ server });
function isRoomId(value) {
return typeof value === "string" && /^[a-zA-Z0-9:_-]{1,64}$/.test(value);
}
function validateClientMessage(value) {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return { ok: false, error: "message must be an object" };
}
if (typeof value.type !== "string") {
return { ok: false, error: "type is required" };
}
if (value.type === "auth") {
if (typeof value.token !== "string" || value.token.length < 8) {
return { ok: false, error: "auth.token is invalid" };
}
return { ok: true, value: { type: "auth", token: value.token } };
}
if (value.type === "join") {
if (!isRoomId(value.roomId)) return { ok: false, error: "roomId is invalid" };
return { ok: true, value: { type: "join", roomId: value.roomId } };
}
if (value.type === "chat") {
if (!isRoomId(value.roomId)) return { ok: false, error: "roomId is invalid" };
if (typeof value.text !== "string" || value.text.length < 1 || value.text.length > 1000) {
return { ok: false, error: "text must be 1-1000 chars" };
}
return {
ok: true,
value: {
type: "chat",
roomId: value.roomId,
requestId: typeof value.requestId === "string" ? value.requestId.slice(0, 100) : randomUUID(),
text: value.text,
},
};
}
if (value.type === "ping") return { ok: true, value: { type: "ping" } };
return { ok: false, error: `unsupported type: ${value.type}` };
}
function decodeMessage(raw) {
if (raw.length > 8192) return { ok: false, error: "message too large" };
try {
return validateClientMessage(JSON.parse(raw.toString("utf8")));
} catch {
return { ok: false, error: "invalid JSON" };
}
}
function sendJson(ws, payload) {
if (ws.readyState !== WebSocket.OPEN) return;
if (ws.bufferedAmount > MAX_BUFFERED_BYTES) {
ws.close(1013, "slow consumer");
return;
}
ws.send(JSON.stringify(payload));
}
function joinRoom(ws, roomId) {
if (!rooms.has(roomId)) rooms.set(roomId, new Set());
rooms.get(roomId).add(ws);
ws.rooms.add(roomId);
}
function leaveAllRooms(ws) {
for (const roomId of ws.rooms) {
const members = rooms.get(roomId);
if (!members) continue;
members.delete(ws);
if (members.size === 0) rooms.delete(roomId);
}
ws.rooms.clear();
}
function broadcast(roomId, payload) {
const members = rooms.get(roomId);
if (!members) return;
for (const client of members) sendJson(client, payload);
}
wss.on("connection", (ws) => {
ws.id = randomUUID();
ws.user = null;
ws.rooms = new Set();
ws.isAlive = true;
const authTimer = setTimeout(() => {
if (!ws.user) ws.close(4401, "auth required");
}, AUTH_TIMEOUT_MS);
ws.on("pong", () => {
ws.isAlive = true;
});
ws.on("message", (raw) => {
const decoded = decodeMessage(raw);
if (!decoded.ok) {
ws.close(1008, decoded.error);
return;
}
const msg = decoded.value;
if (!ws.user) {
if (msg.type !== "auth" || msg.token !== DEV_TOKEN) {
ws.close(4403, "auth failed");
return;
}
ws.user = { id: `user-${ws.id.slice(0, 8)}` };
clearTimeout(authTimer);
sendJson(ws, { type: "auth_ok", connectionId: ws.id, userId: ws.user.id });
return;
}
if (msg.type === "join") {
joinRoom(ws, msg.roomId);
sendJson(ws, { type: "joined", roomId: msg.roomId });
return;
}
if (msg.type === "chat") {
if (!ws.rooms.has(msg.roomId)) {
ws.close(1008, "join room before chat");
return;
}
const event = {
type: "chat",
id: randomUUID(),
requestId: msg.requestId,
roomId: msg.roomId,
userId: ws.user.id,
text: msg.text,
sentAt: new Date().toISOString(),
};
console.log(JSON.stringify({ audit: true, event }));
broadcast(msg.roomId, event);
return;
}
if (msg.type === "ping") sendJson(ws, { type: "pong", sentAt: new Date().toISOString() });
});
ws.on("close", () => {
clearTimeout(authTimer);
leaveAllRooms(ws);
});
});
setInterval(() => {
for (const ws of wss.clients) {
if (ws.isAlive === false) {
leaveAllRooms(ws);
ws.terminate();
continue;
}
ws.isAlive = false;
ws.ping();
}
}, HEARTBEAT_INTERVAL_MS).unref();
server.listen(PORT, () => {
console.log(`WebSocket server listening on ws://localhost:${PORT}`);
});
Client navigateur avec reconnexion
Le constructeur WebSocket du navigateur ne permet pas d’ajouter un header Authorization. En pratique, demandez un ticket court via HTTPS puis envoyez-le dans le premier message auth sur wss. Ne mettez pas les secrets dans les query strings.
<!doctype html>
<html lang="fr">
<body>
<form id="form">
<input id="text" autocomplete="off" placeholder="message" />
<button>send</button>
</form>
<pre id="log"></pre>
<script>
const roomId = "room:demo";
const token = "dev-token";
const log = document.querySelector("#log");
const form = document.querySelector("#form");
const input = document.querySelector("#text");
let socket;
let attempt = 0;
let stopped = false;
function write(line) {
log.textContent += `${line}\n`;
}
function reconnectDelay() {
const base = Math.min(1000 * 2 ** attempt, 10000);
attempt += 1;
return base + Math.floor(Math.random() * 300);
}
function send(payload) {
if (!socket || socket.readyState !== WebSocket.OPEN) return false;
if (socket.bufferedAmount > 512 * 1024) {
write("send skipped: browser buffer is high");
return false;
}
socket.send(JSON.stringify(payload));
return true;
}
function connect() {
socket = new WebSocket("ws://localhost:8080");
write("connecting");
socket.addEventListener("open", () => {
attempt = 0;
send({ type: "auth", token });
send({ type: "join", roomId });
});
socket.addEventListener("message", (event) => {
const msg = JSON.parse(event.data);
write(`${msg.type}: ${JSON.stringify(msg)}`);
});
socket.addEventListener("close", (event) => {
write(`closed code=${event.code} reason=${event.reason}`);
if (!stopped) window.setTimeout(connect, reconnectDelay());
});
socket.addEventListener("error", () => {
write("socket error");
});
}
form.addEventListener("submit", (event) => {
event.preventDefault();
const text = input.value.trim();
if (!text) return;
send({ type: "chat", roomId, requestId: crypto.randomUUID(), text });
input.value = "";
});
window.addEventListener("beforeunload", () => {
stopped = true;
if (socket && socket.readyState === WebSocket.OPEN) socket.close(1000, "page unload");
});
connect();
</script>
</body>
</html>
Revoir le schema de messages a part
Demandez a Claude Code de relire le schema avant le serveur complet. C’est la que passent souvent les gros payloads, les types inconnus et les roomId falsifies.
export function validateClientMessage(value) {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return { ok: false, error: "message must be an object" };
}
if (value.type === "join" && /^[a-zA-Z0-9:_-]{1,64}$/.test(value.roomId)) {
return { ok: true, value: { type: "join", roomId: value.roomId } };
}
if (value.type === "chat" && /^[a-zA-Z0-9:_-]{1,64}$/.test(value.roomId)) {
const text = typeof value.text === "string" ? value.text.trim() : "";
if (text.length >= 1 && text.length <= 1000) {
return { ok: true, value: { type: "chat", roomId: value.roomId, text } };
}
}
return { ok: false, error: "invalid realtime message" };
}
Heartbeat et consommateurs lents
Le heartbeat verifie si la connexion vit encore. Avec ws, le serveur envoie ping et le navigateur repond automatiquement par pong.
setInterval(() => {
for (const ws of wss.clients) {
if (ws.isAlive === false) {
ws.terminate();
continue;
}
ws.isAlive = false;
ws.ping();
}
}, 30000);
Un consommateur lent reste connecte mais n’absorbe plus les messages. Avant chaque envoi, regardez bufferedAmount; s’il est trop eleve, fermez avec 1013 et laissez le client se reconnecter.
Client de test
Ce client simule un deuxieme participant sans ouvrir un autre navigateur.
import { WebSocket } from "ws";
const ws = new WebSocket("ws://localhost:8080");
const roomId = "room:demo";
ws.on("open", () => {
ws.send(JSON.stringify({ type: "auth", token: "dev-token" }));
ws.send(JSON.stringify({ type: "join", roomId }));
setInterval(() => {
ws.send(JSON.stringify({
type: "chat",
roomId,
requestId: crypto.randomUUID(),
text: `hello ${new Date().toISOString()}`,
}));
}, 3000);
});
ws.on("message", (data) => {
console.log(data.toString());
});
Lancez node test-client.mjs, redemarrez le serveur et verifiez que le navigateur se reconnecte avec backoff.
Pieges concrets
- Mettre un secret dans
ws://example.com?token=..., ce qui le propage dans logs et captures. - Faire seulement
JSON.parsesans valider type, taille, room et longueur du texte. - Envoyer sans verifier
readyState. - Oublier le heartbeat et garder des connexions mortes.
- Confondre broadcast WebSocket et journal d’audit. Les evenements importants doivent etre stockes durablement.
- Demander a Claude Code “fais un chat” sans exiger auth, reconnexion, backpressure, close codes et tests.
Prompt de revue Claude Code
Review this WebSocket implementation before release.
Scope:
- server.mjs
- browser client
- message schema validator
Check:
1. Can a client join/chat before auth?
2. Can secrets leak into URLs, logs, or error messages?
3. Can a client spoof roomId and send to another room?
4. Can reconnect/backoff create a connection storm?
5. Does heartbeat remove dead connections?
6. Are slow consumers handled with bufferedAmount?
7. Is there an audit log outside WebSocket delivery?
Return:
- P0/P1/P2 findings first
- Reproduction steps
- Minimal patch
- Missing tests
Pour prolonger le sujet, lisez aussi Claude Code security best practices, code review checklist et webhook implementation.
Formation, templates et conseil
L’exemple suffit pour un PoC local. Pour une equipe, il faut aussi definir CLAUDE.md, les regles de revue, la politique de close codes, le stockage d’audit et le rollback. ClaudeCodeLab propose training and consultation ainsi que templates and product guides pour transformer l’automatisation temps reel en pratique d’equipe.
Conclusion
WebSocket convient aux flux de progression Claude Code, aux rooms de revue, aux outils collaboratifs et aux alertes operationnelles. Mais une connexion ouverte n’est pas une garantie de fiabilite. Authentification, room routing, validation, reconnexion, heartbeat, backpressure et audit doivent etre concus ensemble.
En testant localement l’implementation de cet article, chat avant auth a ete rejete, le navigateur s’est reconnecte apres un redemarrage serveur, et les broadcasts sont restes dans la bonne room. Les seuils de buffer, les messages de close code et le stockage d’audit restent a adapter a chaque equipe. Le retour pratique de Masa : Claude Code devient nettement plus utile quand on lui demande de chercher les modes d’echec, pas seulement de generer le chemin heureux.
PDF gratuit: cheatsheet Claude Code
Saisissez votre email et téléchargez une page avec commandes, habitudes de review et workflow sûr.
Nous protégeons vos données et n'envoyons pas de spam.
À propos de l'auteur
Masa
Ingénieur spécialisé dans les workflows pratiques avec Claude Code.
Articles liés
Workflow Obsidian vers CLAUDE.md avec Claude Code
Transformer des notes Obsidian en notes CLAUDE.md concises pour reprendre les sessions sans réexpliquer.
Claude Code Revenue CTA Routing : relier articles, PDF, Gumroad et consultation
Un workflow Claude Code pour orienter les lecteurs vers PDF gratuit, Gumroad ou consultation selon l'intention.
Règles de handoff Claude Code en équipe: preuves, permissions, rollback et revenus
Un format concret pour transmettre un travail Claude Code avec preuves, permissions, rollback, PDF gratuit, Gumroad et consultation.