WebSocket em tempo real com Claude Code: auth, reconexao, heartbeat e rooms
Implemente WebSocket confiavel com Claude Code: autenticacao, reconexao, heartbeat, rooms, validacao e revisao.
WebSocket faz sentido quando a tela precisa mudar assim que o servidor sabe de algo: um job do Claude Code entra em testes, um comentario aparece na revisao, um usuario entra em uma room ou um alerta operacional precisa aparecer na hora. Claude Code cria um rascunho rapido, mas uma funcao realtime pronta para producao precisa de mais do que um onmessage funcionando.
Os problemas mais comuns ficam em autenticacao, tempestades de reconexao, conexoes moveis mortas, JSON invalido, consumidores lentos, vazamento entre rooms e ausencia de logs de auditoria. Este artigo traz um servidor Node.js com ws, cliente de navegador com backoff, validador de mensagens, heartbeat, cliente de teste e prompt de revisao para Claude Code.
Como referencia oficial, use MDN WebSocket, readyState e close. Para trabalhar melhor com Claude Code, veja Claude Code common workflows.
Quando usar WebSocket
WebSocket e um canal bidirecional de longa duracao. Ele nao substitui HTTP API, filas, escrita em banco, pagamentos ou auditoria. Use para coordenacao rapida; deixe eventos importantes em caminhos duraveis no servidor.
| Opcao | Melhor uso | Evite para | Instrucao para Claude Code |
|---|---|---|---|
| WebSocket | Chat, UI colaborativa, progresso ao vivo, broadcast por room | Notificacoes raras, historico pesquisavel | Incluir auth, reconexao, heartbeat e backpressure |
| Server-Sent Events | Eventos do servidor para o navegador | Acoes frequentes do navegador para o servidor | Projetar IDs e tipos de evento |
| Polling | Admin que atualiza a cada 15-60 segundos | UX de baixa latencia | Adicionar cache e rate limit |
| HTTP API | Salvar, pagamento, auditoria, admin | Typing indicator ou presenca frequente | Reutilizar as mesmas regras de autorizacao |
Casos praticos: mostrar progresso de tarefas longas do Claude Code, sincronizar comentarios e leitura em salas de revisao, e transmitir alertas operacionais por cliente ou equipe. Em treinamentos, tambem serve para o instrutor ver o status de exercicios de cada participante em tempo real.
Arquitetura
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 significa que o receptor nao processa rapido o suficiente e o buffer do remetente cresce. A MDN observa que a API WebSocket classica nao aplica backpressure automaticamente, entao a implementacao precisa observar bufferedAmount.
Inclua modos de falha no prompt
Nao peca apenas “um chat em tempo real”. Defina as restricoes logo no primeiro pedido.
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
Servidor Node ws executavel
Execute localmente:
npm init -y
npm install ws
node server.mjs
Crie server.mjs. Em producao, troque DEV_TOKEN por um ticket de curta duracao, cookie de sessao ou identidade validada pelo 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}`);
});
Cliente de navegador com reconexao e backoff
O construtor WebSocket do navegador nao permite headers Authorization arbitrarios. Em aplicacoes reais, emita um ticket curto via HTTPS e envie no primeiro auth message sobre wss. Nao coloque secrets na query string.
<!doctype html>
<html lang="pt">
<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>
Revise o schema de mensagens separadamente
Peça ao Claude Code para revisar o schema antes do servidor inteiro. Um contrato vago deixa passar payloads grandes, tipos desconhecidos e roomId falsificado.
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 e consumidores lentos
Heartbeat verifica se a conexao ainda esta viva. Com ws, o servidor envia ping e o navegador responde pong no nivel do protocolo.
setInterval(() => {
for (const ws of wss.clients) {
if (ws.isAlive === false) {
ws.terminate();
continue;
}
ws.isAlive = false;
ws.ping();
}
}, 30000);
Consumidor lento continua conectado, mas nao processa mensagens a tempo. Antes de enviar, verifique bufferedAmount; se estiver alto, feche com 1013 e deixe o cliente reconectar depois.
Cliente de teste
Use como segundo participante sem abrir outro navegador.
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());
});
Rode node test-client.mjs, reinicie o servidor e confirme que o navegador reconecta com backoff.
Armadilhas comuns
- Colocar secrets em
ws://example.com?token=..., vazando para logs e screenshots. - Fazer apenas
JSON.parsesem validar tipo, tamanho, room e tamanho do texto. - Enviar sem verificar
readyState. - Ignorar heartbeat e manter conexoes mortas.
- Tratar broadcasts WebSocket como auditoria. Eventos importantes precisam de armazenamento duravel.
- Pedir ao Claude Code “faça um chat” sem exigir auth, reconexao, backpressure, close codes e testes.
Prompt de revisao para 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
Para continuar, veja Claude Code security best practices, code review checklist e webhook implementation.
Treinamento, templates e consultoria
O exemplo basta para um PoC local. Em equipe, tambem e preciso definir CLAUDE.md, regras de revisao, politica de close codes, armazenamento de auditoria e rollback. A ClaudeCodeLab oferece training and consultation e templates and product guides para transformar automacao realtime em fluxo de trabalho real.
Conclusao
WebSocket combina com streams de progresso do Claude Code, salas de revisao, ferramentas colaborativas e alertas operacionais. Mas estar conectado nao significa estar seguro. Trate autenticacao, room routing, validacao, reconexao, heartbeat, backpressure e auditoria como um unico desenho.
Ao testar localmente a implementacao deste artigo, chat antes de auth foi rejeitado, o navegador reconectou apos reiniciar o servidor e os broadcasts ficaram dentro da room correta. O que ainda precisa de ajuste por equipe sao limites de buffer, textos de close code e destino da auditoria. A conclusao pratica de Masa: Claude Code melhora muito a qualidade quando revisa modos de falha, nao apenas quando gera o caminho feliz.
PDF grátis: cheatsheet do Claude Code
Informe seu e-mail e baixe uma página com comandos, hábitos de revisão e workflows seguros.
Cuidamos dos seus dados e não enviamos spam.
Sobre o autor
Masa
Engenheiro focado em workflows práticos com Claude Code.
Artigos relacionados
Workflow Obsidian para CLAUDE.md com Claude Code
Transforme notas de trabalho do Obsidian em notas operacionais CLAUDE.md para preservar contexto.
Claude Code Revenue CTA Routing: artigos para PDF, Gumroad e consultoria
Um fluxo com Claude Code para levar leitores ao PDF grátis, Gumroad ou consultoria conforme intenção.
Regras de handoff para equipes com Claude Code: evidências, permissões, rollback e receita
Formato prático para entregar trabalho do Claude Code com prova, permissões, rollback, PDF grátis, Gumroad e consultoria.