Docs/Foundations/Raw Socket Client
Foundations

Raw socket client

The engine speaks plain Socket.IO. Everything on the per-game pages that follow assumes you've read this page first β€” the connection handshake, reconnection behavior, and one specific footgun are the same across every game.

There's an official SDK (@beta-gamer/react) that wraps all of this. It's not optimized for the current hosted-matchmaking architecture yet, so this reference documents the protocol directly β€” enough to build a fully working client without it. Once the SDK catches up, this page doesn't go away; it becomes the reference the SDK itself is built on top of.

Connecting

Each game has its own Socket.IO namespace. Connect with a sessionToken from session creation β€” never an API key.

js
import { io } from 'socket.io-client';

const socket = io('https://api.beta-gamer.com/checkers', {
  auth: { token: sessionToken },
  autoConnect: false,   // see the Strict Mode warning below before removing this
});

socket.connect();

What the server checks, in order

CheckRejected with
Token present and a stringunauthorized: missing session token
Token verifies (not expired, valid signature)unauthorized: invalid or expired session token
Token’s game claim matches this namespaceunauthorized: token issued for a different game
Token carries a playerId claimunauthorized: token missing playerId claim

A token minted for /checkers will not authenticate against /pool β€” each token is scoped to one game. On rejection, the server calls next(new Error(...)), which surfaces client-side as a connect_error event with that message.

js
socket.on('connect_error', (err) => {
  console.error('Connection rejected:', err.message);
});

The Strict Mode footgun

This is not hypothetical β€” it was found live, in this codebase's own reference client, more than once. React 18 (and 19's dev mode) intentionally mounts every component twice. If you create a socket inside a plain useEffect with no guard, you get two real connections β€” the first one immediately orphaned, its variables still referenced by stale closures. Symptoms: a shot or move that silently does nothing, or fires against a dead connection.

The fix that held up under testing β€” autoConnect: false, deferred connect(), and deliberately no "already initialized" guard:

tsx
useEffect(() => {
  const s = io('https://api.beta-gamer.com/checkers', {
    auth: { token: sessionToken },
    autoConnect: false,
  });
  socketRef.current = s;
  s.connect();

  return () => { s.disconnect(); };
}, [sessionToken]);
The instinct to add an isInitialized ref to prevent a "double" connect is exactly backwards β€” it blocks Strict Mode's second mount from ever creating its socket, leaving only the first, doomed one alive. No guard, autoConnect: false, and cleanup that actually calls disconnect() is the whole fix.

Disconnection and reconnection

A dropped connection doesn't end the match immediately. The server holds the player's seat open for a grace window β€” reconnecting with the same sessionToken within that window resumes the game in place.

GameReconnect window
Pool60 seconds
Checkers / Tic-tac-toe / Connect 4 / Fight Arena / LudoVaries by game β€” see each game’s own page
js
// Reconnect with the same token
socket.on('connect', () => {
  socket.emit('game:reconnect', { roomId, playerId });
});
socket.on('game:reconnected', (state) => {
  // full current game state β€” rehydrate your UI from this
});
Not every game exposes an explicit game:reconnect event β€” Pool does; check each game's own events page before assuming this exact shape applies everywhere.

Common events, every game

These event names and payloads are shared verbatim β€” verified identical across all six games' socket handlers, not just similar.

EventDirectionPayload
errorserver β†’ client{ message: string }
matchmaking:joinclient β†’ server{ username?: string, playerId: string }
matchmaking:leaveclient β†’ servernone
matchmaking:already_startedserver β†’ clientSent instead of matching you again if a match is already underway.
game:startedserver β†’ clientGame-specific shape β€” see each game’s own page. Always includes roomId, players, playerId.
game:overserver β†’ clientSame result shape as the game.ended webhook β€” see Webhooks.

Next

Once connected, each game's own page has the exact events, hosted-session properties, and rules you need. Start with whichever game you're building against.