Claude Code WebSocket रियलटाइम: auth, reconnect, heartbeat और rooms
Claude Code से सुरक्षित WebSocket realtime बनाएं: auth, reconnect, heartbeat और validation सहित।
WebSocket तब उपयोगी है जब UI को server की नई स्थिति तुरंत दिखानी हो: Claude Code job testing phase में गया, review room में नया comment आया, कोई user room में जुड़ा, या operations alert तुरंत दिखाना है। Claude Code पहला draft जल्दी बना सकता है, लेकिन production-ready realtime feature के लिए सिर्फ onmessage चलना काफी नहीं है।
असली समस्याएं authentication handshake, reconnect storm, mobile network disconnect, invalid JSON, slow consumer, room leakage और audit log की कमी में आती हैं। इस लेख में Node.js ws server, browser reconnect client, message schema validator, heartbeat pattern, test client और Claude Code review prompt copy-paste योग्य रूप में दिए गए हैं।
WebSocket API की बुनियाद के लिए MDN WebSocket, connection state के लिए readyState, और close behavior के लिए close देखें। Claude Code workflow के लिए Claude Code common workflows भी उपयोगी है।
WebSocket कब चुनें
WebSocket long-lived, bidirectional channel है। यह HTTP API, durable queue, database write, payment flow या audit log का replacement नहीं है। Fast coordination के लिए WebSocket रखें, लेकिन important business events को durable server-side path पर ही save करें।
| विकल्प | सबसे अच्छा उपयोग | किससे बचें | Claude Code को निर्देश |
|---|---|---|---|
| WebSocket | chat, collaborative UI, live job progress, room broadcast | rare notifications, searchable history | auth, reconnect, heartbeat, backpressure शामिल करें |
| Server-Sent Events | server से browser तक one-way updates | browser से बार-बार actions | event ID और event type design करें |
| Polling | admin page जो 15-60 sec में refresh हो | low-latency UX | cache और rate limit जोड़ें |
| HTTP API | save, payment, audit, admin changes | typing indicators या high-frequency presence | वही authorization checks reuse करें |
व्यावहारिक use cases कम से कम तीन हैं। पहला, लंबी Claude Code task का live progress: analyzing, testing, diff checking। दूसरा, review room में comments और read state तुरंत update करना। तीसरा, operations dashboard में customer या team के हिसाब से alerts broadcast करना। Training में instructor प्रत्येक participant का exercise status realtime देख सकता है।
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
Backpressure का मतलब है receiver messages को पर्याप्त तेजी से process नहीं कर पा रहा और sender का buffer बढ़ रहा है। MDN बताता है कि classic WebSocket API backpressure automatically handle नहीं करती, इसलिए implementation में bufferedAmount देखना जरूरी है।
Prompt में failure modes शामिल करें
Claude Code से सिर्फ “chat बना दो” न कहें। शुरुआत में ही constraints दें।
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
Runnable Node ws server
Local run:
npm init -y
npm install ws
node server.mjs
server.mjs बनाएं। Production में fixed DEV_TOKEN की जगह short-lived ticket, session cookie, या gateway-verified identity लगाएं।
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}`);
});
Browser client: reconnect और backoff
Browser का WebSocket constructor arbitrary Authorization header नहीं लगा सकता। इसलिए browser app में HTTPS से short-lived ticket लें और wss connect होने के बाद पहली auth message में भेजें। Secret को query string में न डालें, क्योंकि proxy, access log और analytics tool उसे capture कर सकते हैं।
<!doctype html>
<html lang="hi">
<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>
Message schema अलग से review करें
Claude Code से पहले schema review कराएं, फिर पूरा server। अस्पष्ट schema में large payload, unknown type और fake roomId आसानी से छूट जाते हैं।
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 और slow consumers
Heartbeat connection जीवित है या नहीं, यह जांचता है। ws server ping भेजता है और browser protocol level पर अपने आप pong देता है।
setInterval(() => {
for (const ws of wss.clients) {
if (ws.isAlive === false) {
ws.terminate();
continue;
}
ws.isAlive = false;
ws.ping();
}
}, 30000);
Slow consumer connected तो रहता है, पर incoming messages समय पर process नहीं करता। हर send से पहले bufferedAmount देखें; बहुत ज्यादा हो तो 1013 से close करें और client को बाद में reconnect करने दें।
Test client
दूसरा participant बनाने के लिए browser खोलने की जरूरत नहीं है।
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());
});
node test-client.mjs चलाएं, server restart करें और देखें कि browser backoff के साथ reconnect करता है या नहीं।
आम गलतियां
- Secret को
ws://example.com?token=...में डालना, जिससे वह logs और screenshots में जा सकता है। - सिर्फ
JSON.parseकरना, लेकिन type, size, room और text length validate न करना। readyStateदेखे बिनाsendकरना।- heartbeat छोड़ देना और dead mobile connections को जीवित मानते रहना।
- WebSocket broadcast को audit log समझना। Important events को durable storage में save करना चाहिए।
- Claude Code को केवल “chat बना दो” कहना और auth, reconnect, backpressure, close code, tests भूल जाना।
Claude Code review prompt
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
आगे पढ़ने के लिए Claude Code security best practices, code review checklist और webhook implementation देखें।
Training, templates और consultation
यह sample local PoC के लिए काफी है। Team rollout में CLAUDE.md, review rules, close-code policy, audit storage और rollback strategy भी चाहिए। ClaudeCodeLab training and consultation और templates and product guides देता है ताकि realtime automation को वास्तविक workflow में सुरक्षित रूप से जोड़ा जा सके।
निष्कर्ष
WebSocket Claude Code progress streams, review rooms, collaborative tools और operational alerts के लिए अच्छा fit है। लेकिन connection खुल जाना reliability या security की guarantee नहीं है। Auth handshake, room routing, validation, reconnect, heartbeat, backpressure और audit logging को एक design के रूप में संभालें।
इस लेख की implementation को local पर आजमाने के बाद auth से पहले भेजा गया chat reject हुआ, server restart के बाद browser exponential backoff से reconnect हुआ, और room broadcasts उसी room तक सीमित रहे। Team-specific tuning फिर भी चाहिए: buffer threshold, close code wording और audit event storage। Masa का practical takeaway यह है कि Claude Code से केवल happy path generate कराने के बजाय failure modes review कराना quality को ज्यादा बढ़ाता है।
मुफ़्त PDF: Claude Code cheatsheet
Email डालें और commands, review habits तथा safe workflow वाली एक-page PDF पाएँ.
हम आपका data सुरक्षित रखते हैं और spam नहीं भेजते.
लेखक के बारे में
Masa
Claude Code workflow और team adoption पर काम करने वाला engineer.
संबंधित लेख
Claude Code Obsidian to CLAUDE.md workflow: context बार-बार न समझाएं
Obsidian notes को CLAUDE.md operating notes में बदलकर Claude Code sessions को resume करना आसान बनाएं.
Claude Code Revenue CTA Routing: article से PDF, Gumroad और consultation तक
Reader intent के आधार पर free PDF, Gumroad products और consultation तक CTA route करने वाला workflow.
Claude Code टीम हैंडऑफ नियम: review proof, permissions, rollback और revenue path
Claude Code टीम काम के लिए evidence, permission rules, rollback, free PDF, Gumroad और consultation path वाला handoff.