Quick start — React
End-to-end: from an API key to a live, running match — no SDK required.
@beta-gamer/react) on npm right now, but be honest with yourself before reaching for it: it predates the current hosted-matchmaking architecture and hasn't been updated for it. Installing it today will not give you a working integration. It's being rebuilt in a separate repository and will come back here once it matches what's actually live. Until then, this guide — plain socket.io-client plus about twenty lines of your own React — is the real, working path, not a fallback.Get your API key
Register at beta-gamer.com/register. You'll get a live key (bg_live_…) and a test key (bg_test_…). Use the test key while developing — test sessions are in-memory only and never touch the database, so there's nothing to clean up while you're iterating.
Install the one real dependency
That's it — the actual Socket.IO client, nothing Beta-Gamer-specific:
npm install socket.io-clientCreate a session (your backend)
Call POST /v1/sessions from your server, not the browser. Return the resulting sessionToken to your frontend — that token is what proves the browser is allowed to join this specific match, without ever handing it your API key.
// e.g. a Next.js API route, or any backend endpoint you control
const res = await fetch('https://api.beta-gamer.com/v1/sessions', {
method: 'POST',
headers: {
'Authorization': 'Bearer bg_live_xxxx', // server only, never sent to the browser
'Content-Type': 'application/json',
},
body: JSON.stringify({
game: 'checkers', // checkers | tictactoe | connect4 | fight-arena | pool | ludo
matchType: 'matchmaking',
players: [{ id: user.id, displayName: user.name }],
}),
});
const { sessionToken, sessionId } = await res.json();
// send sessionToken (not sessionId) down to your React frontendWrite a small connection hook
This is the piece an SDK would normally hand you. It's short enough to own yourself, and owning it means you're never blocked waiting on someone else's package. The autoConnect: false and the deliberate absence of an "already connected" guard both matter — see Raw Socket Client for exactly why, it's a real bug this exact pattern was written to avoid, not a stylistic choice.
// useBetaGamerSocket.ts
import { useEffect, useRef, useState } from 'react';
import { io, Socket } from 'socket.io-client';
export function useBetaGamerSocket(game: string, sessionToken: string | null) {
const socketRef = useRef<Socket | null>(null);
const [connected, setConnected] = useState(false);
useEffect(() => {
if (!sessionToken) return;
const socket = io(`https://api.beta-gamer.com/${game}`, {
auth: { token: sessionToken },
autoConnect: false,
});
socketRef.current = socket;
socket.on('connect', () => setConnected(true));
socket.on('disconnect', () => setConnected(false));
socket.connect();
return () => {
socket.disconnect();
socketRef.current = null;
};
}, [game, sessionToken]);
return { socket: socketRef.current, connected };
}Join matchmaking and listen for the match starting
Once connected, emit matchmaking:join, then listen for game:started. Every game's own page has the exact game:started payload — checkers' is used below as the example.
import { useEffect, useState } from 'react';
import { useBetaGamerSocket } from './useBetaGamerSocket';
export function CheckersGame({ sessionToken, playerId, displayName }: {
sessionToken: string; playerId: string; displayName: string;
}) {
const { socket, connected } = useBetaGamerSocket('checkers', sessionToken);
const [status, setStatus] = useState('Connecting…');
const [roomId, setRoomId] = useState('');
useEffect(() => {
if (!socket || !connected) return;
socket.emit('matchmaking:join', { playerId, username: displayName });
setStatus('Waiting for opponent…');
socket.on('game:started', ({ roomId: newRoomId, color }) => {
setRoomId(newRoomId);
setStatus(`Game started — you're ${color}`);
});
socket.on('game:over', ({ winner }) => {
setStatus(winner === playerId ? 'You won!' : winner ? 'You lost' : 'Draw');
});
return () => {
socket.off('game:started');
socket.off('game:over');
};
}, [socket, connected]);
return <p>{status}</p>;
}connected, not on mount — emitting before the socket has actually finished connecting is a silent no-op, not an error, which makes it a genuinely confusing bug to track down the first time you hit it.Handle the session token on page load
Session tokens are single-use. If someone refreshes a page mid-game or comes back to a finished one, validate first so they see the result instead of the socket trying (and failing) to rejoin a match that's already over.
useEffect(() => {
fetch(`https://api.beta-gamer.com/v1/sessions/validate?token=${sessionToken}`)
.then(r => r.json())
.then(d => setStatus(d.status === 'ended' ? 'ended' : 'ok'))
.catch(() => setStatus('ok'));
}, [sessionToken]);Receive webhooks (optional)
When a live-mode game ends, a signed payload lands on your server automatically — useful for updating a leaderboard or awarding rewards without your frontend having to report anything back itself. Full event list and payload shapes: Webhooks.
app.post('/webhooks/beta-gamer', (req, res) => {
const { event, data } = req.body;
// event: 'session.created' | 'session.started' | 'player.joined' | 'player.left' | 'game.ended'
res.sendStatus(200);
});Next: pick your game from the Games catalog for its exact events and rules, or read Raw Socket Client in full for reconnection handling and the Strict Mode issue this guide's hook already avoids.