Multiplayer Tic-Tac-Toe with Nakama (server-authoritative match) and a React client.
- Backend: Nakama + Postgres
- Runtime: Nakama TypeScript modules compiled to JavaScript
- Frontend: React + Vite + Nakama JS client
- Realtime transport: Nakama sockets
docker-compose.yml- Local Nakama and Postgres containersdeploy/- Production Docker image and Render/Railway hints (deploy/README.md); entrypoint script lives innakama/docker-entrypoint.shnakama/- Runtime source and TypeScript build confignakama/modules/index.ts- Nakama JS entrypoint only: match logic,create_tic_tac_toe_matchRPC, andInitModule(compiled toindex.js; Nakama does not load other.jsfiles as entrypoints)frontend/- Web clientfrontend/src/env.ts- Reads public Nakama settings fromVITE_*env varsfrontend/src/nakama.ts- Nakama client (device auth, socket, RPC, match messages)frontend/src/App.tsx- UIfrontend/.env.example- Template for production buildsfrontend/.env.development- Defaults for localnpm run devagainst Docker Nakama
- All game rules and
InitModulelive inindex.tsbecause Nakama’s JavaScript runtime loads onlyindex.jsfrom the module path. - The web client connects to Nakama automatically on load (no manual “connect” step).
- Clients send move intent (
index) with opcode1, and restart requests with opcode3. - Server validates:
- match capacity (2 players max)
- turn ownership
- valid board index
- unoccupied cells
- Server computes turn changes and winner detection.
- Server broadcasts authoritative state using:
TextEncoderon backend (with a small fallback whenTextEncoderis unavailable in the JS runtime)dispatcher.broadcastMessage(2, Uint8ArrayPayload)
- Frontend decodes state updates using
TextDecoderfromsocket.onmatchdata.
- Docker + Docker Compose
- Node.js 20+ and npm
cd nakama
npm install
npm run buildcd ..
docker-compose up --buildNakama endpoints:
- HTTP API:
http://127.0.0.1:7350 - Console:
http://127.0.0.1:7351 - Realtime socket:
ws://127.0.0.1:7350/ws
cd frontend
npm install
npm run devVite default URL: http://127.0.0.1:5173
Local dev loads frontend/.env.development. For a production bundle, set VITE_NAKAMA_HOST, VITE_NAKAMA_PORT, VITE_NAKAMA_SERVER_KEY, and VITE_NAKAMA_USE_SSL (true or false) in the build environment or in an untracked frontend/.env.production copied from frontend/.env.example. npm run build fails if any of these are missing.
- Open two browser windows (or one normal + one incognito). Each tab loads the app and connects to Nakama on its own.
- In window A click Create Match.
- Copy match ID and use Join Match in window B, or use Discover Open Matches.
- Click board cells to send moves.
- Confirm:
- turns switch correctly
- invalid moves are ignored
- state updates in both windows in real-time
- winner/draw appears correctly
- Restart / Restart Round syncs a new game via opcode
3
- Backend: Nakama + Postgres (VM, Kubernetes, or managed containers).
- Frontend: static hosting; configure the same four
VITE_NAKAMA_*variables in the host’s build settings so the client points at your Nakama HTTP/WebSocket endpoint. - Run
npm run buildinnakama/before deploying the game server. - Point the Nakama container’s module path at the built JS (see
docker-compose.yml).
- Server key:
defaultkey - Match handler name:
tic_tac_toe - Match data opcodes:
1: player move input (client → server)2: authoritative state update (server → clients)3: restart request when both players are present (client → server); server resets board and turn