Use Cases (Diperbarui: 1/6/2026)

WebSocket realtime dengan Claude Code: auth, reconnect, heartbeat, dan rooms

Bangun WebSocket realtime yang aman dengan Claude Code: auth, reconnect, heartbeat, rooms, validasi, dan review.

WebSocket realtime dengan Claude Code: auth, reconnect, heartbeat, dan rooms

WebSocket cocok saat layar harus berubah begitu server mengetahui sesuatu: job Claude Code masuk tahap test, komentar review baru muncul, pengguna bergabung ke room, atau alert operasional harus tampil segera. Claude Code bisa membuat draft awal dengan cepat, tetapi fitur realtime yang siap produksi membutuhkan lebih dari sekadar onmessage yang berjalan.

Bagian yang sering rusak adalah authentication handshake, reconnect storm, koneksi mobile yang mati diam-diam, JSON tidak valid, slow consumer, kebocoran antar-room, dan audit log yang tidak ada. Artikel ini memberi contoh Node.js ws, client browser dengan backoff, validator schema pesan, heartbeat, test client, dan prompt review untuk Claude Code.

Untuk dasar API, buka MDN WebSocket, readyState, dan close. Untuk disiplin kerja Claude Code, lihat Claude Code common workflows.

Kapan WebSocket Tepat Dipakai

WebSocket adalah channel dua arah yang hidup lama. Ia tidak menggantikan HTTP API, queue, penulisan database, billing flow, atau audit log. Pakai WebSocket untuk koordinasi cepat; event bisnis penting tetap harus disimpan lewat jalur server yang durable.

PilihanCocok untukHindari untukInstruksi ke Claude Code
WebSocketChat, UI kolaboratif, progress live, broadcast per roomNotifikasi jarang, riwayat yang harus dicariSertakan auth, reconnect, heartbeat, backpressure
Server-Sent EventsUpdate satu arah dari server ke browserAksi browser yang sering ke serverDesain event ID dan event type
PollingAdmin panel yang refresh tiap 15-60 detikUX latensi rendahTambahkan cache dan rate limit
HTTP APISave, payment, audit, admin changesTyping indicator atau presence tinggiPakai ulang authorization check yang sama

Use case praktisnya minimal tiga. Pertama, live progress untuk task Claude Code yang lama: analyzing, testing, checking diff. Kedua, review room tempat komentar dan read state muncul langsung. Ketiga, dashboard operasional yang mengirim alert per customer atau team. Untuk training, instruktur juga bisa melihat status latihan setiap peserta secara realtime.

Arsitektur

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 berarti penerima tidak mampu memproses cukup cepat sehingga buffer pengirim membesar. MDN mencatat bahwa WebSocket API klasik tidak otomatis menerapkan backpressure, jadi implementasi perlu memantau bufferedAmount.

Masukkan Failure Mode ke Prompt

Jangan hanya meminta “buat chat realtime”. Berikan batasan sejak awal.

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

Server Node ws yang Bisa Dijalankan

Jalankan lokal:

npm init -y
npm install ws
node server.mjs

Buat server.mjs. Di production, ganti DEV_TOKEN dengan short-lived ticket, session cookie, atau identity yang sudah diverifikasi 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 Browser dengan Reconnect dan Backoff

Constructor WebSocket di browser tidak bisa menambahkan header Authorization sembarang. Untuk aplikasi browser, ambil short-lived ticket melalui HTTPS, lalu kirim sebagai message auth pertama di atas wss. Jangan simpan secret di query string.

<!doctype html>
<html lang="id">
  <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>

Review Schema Pesan Secara Terpisah

Minta Claude Code meninjau schema sebelum seluruh server. Kontrak yang kabur membuat payload besar, type tidak dikenal, dan roomId palsu mudah lolos.

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 dan Slow Consumer

Heartbeat memeriksa apakah koneksi masih hidup. Dengan ws, server mengirim ping dan browser menjawab pong otomatis pada level protokol.

setInterval(() => {
  for (const ws of wss.clients) {
    if (ws.isAlive === false) {
      ws.terminate();
      continue;
    }
    ws.isAlive = false;
    ws.ping();
  }
}, 30000);

Slow consumer tetap tersambung tetapi tidak memproses pesan cukup cepat. Sebelum mengirim, cek bufferedAmount; jika terlalu tinggi, tutup dengan 1013 agar client mencoba lagi nanti.

Test Client

Gunakan ini sebagai peserta kedua tanpa membuka browser lain.

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());
});

Jalankan node test-client.mjs, restart server, lalu pastikan browser reconnect dengan backoff.

Kesalahan Umum

  • Menaruh secret di ws://example.com?token=..., sehingga bocor ke log dan screenshot.
  • Hanya memakai JSON.parse tanpa memvalidasi type, ukuran, room, dan panjang teks.
  • Mengirim tanpa mengecek readyState.
  • Tidak ada heartbeat, sehingga koneksi mobile yang mati tetap dianggap hidup.
  • Menganggap broadcast WebSocket sebagai audit log. Event penting tetap perlu storage yang durable.
  • Meminta Claude Code “buat chat” tanpa menyebut auth, reconnect, backpressure, close code, dan test.

Prompt Review untuk 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

Untuk topik terkait, baca Claude Code security best practices, code review checklist, dan webhook implementation.

Training, Template, dan Konsultasi

Contoh ini cukup untuk PoC lokal. Untuk tim, Anda juga perlu CLAUDE.md, aturan review, kebijakan close code, penyimpanan audit, dan rollback. ClaudeCodeLab menyediakan training and consultation serta templates and product guides untuk membawa realtime automation ke workflow nyata.

Kesimpulan

WebSocket cocok untuk progress stream Claude Code, review room, alat kolaboratif, dan alert operasional. Namun koneksi yang berhasil bukan berarti aman. Auth handshake, room routing, validation, reconnect, heartbeat, backpressure, dan audit logging harus dirancang sebagai satu paket.

Setelah implementasi artikel ini dicoba secara lokal, chat sebelum auth ditolak, browser reconnect setelah server restart, dan broadcast tetap berada di room yang benar. Yang masih perlu disesuaikan per tim adalah threshold buffer, wording close code, dan lokasi penyimpanan audit. Pelajaran praktis dari Masa: Claude Code paling berguna ketika diminta meninjau failure mode, bukan hanya membuat happy path.

#Claude Code #WebSocket #real-time #ws #Node.js
Gratis

PDF gratis: cheatsheet Claude Code

Masukkan email dan unduh satu halaman berisi command, kebiasaan review, dan workflow aman.

Kami menjaga datamu dan tidak mengirim spam.

Masa

Tentang penulis

Masa

Engineer yang berfokus pada workflow Claude Code praktis dan adopsi tim.