A real-time shared grid where players around the world claim tiles simultaneously. Every click is validated server-side and broadcast live to all connected users β no refresh needed.
- How it works
- Project structure
- Run locally
- Environment variables
- Deploy to production
- Nakama admin console
- Troubleshooting
Browser (React + Vite)
β
β WebSocket (persistent)
βΌ
Nakama :7350 β server-authoritative match loop
β validates every tile claim
β broadcasts delta to all players
βΌ
CockroachDB β tile state saved every 5 s
survives server restarts
- One global match is created on startup. Every player joins the same match.
- The server enforces a 3-second cooldown per player β clients cannot cheat.
- Only the changed tile is broadcast per claim, not the full 1 000-tile board.
- Players authenticate automatically via a device ID stored in
localStorageβ no sign-up needed.
pixel-war-game/
βββ client/ # React + Vite frontend
β βββ src/
β β βββ nakama-client.ts # Auth, RPC, WebSocket singleton
β β βββ useGame.ts # Match state + socket events
β β βββ App.tsx # Auth gate + page routing
β β βββ Lobby.tsx # Name + color picker
β β βββ Game.tsx # Game shell + header bar
β β βββ GameBoard.tsx # 40 Γ 25 interactive grid
β β βββ App.css # Dark theme
β βββ .env # Local env vars
β βββ vite.config.ts
β βββ package.json
β
βββ nakama/
β βββ data/modules/index.js # Server-authoritative game logic
β βββ local.yml # Nakama server config
β βββ Dockerfile # Used by Render for the backend
β
βββ docker-compose.yml # Local dev stack
βββ docker-compose.prod.yml # Production overrides
βββ Makefile
| Tool | Min version | Download |
|---|---|---|
| Docker Desktop | 4.x | https://www.docker.com/products/docker-desktop |
| Node.js | 20 | https://nodejs.org |
| npm | 10 | bundled with Node.js |
git clone https://github.com/yourorg/pixel-war-game.git
cd pixel-war-gamedocker compose up -dThis starts two containers:
| Container | Ports | What it is |
|---|---|---|
cockroachdb |
26257, 8081 |
Database |
nakama |
7350, 7351 |
Game server |
CockroachDB takes ~10 s to become healthy. Nakama then runs migrations and loads index.js. Wait until this returns {}:
curl http://localhost:7350/healthcheckOpen client/.env and make sure it looks like this:
VITE_NAKAMA_HOST=127.0.0.1
VITE_NAKAMA_PORT=7350
VITE_NAKAMA_SSL=false
VITE_NAKAMA_KEY=defaultkeyIf your
.envcurrently points at Render (e.g.VITE_NAKAMA_HOST=https://board-game-85nq.onrender.com/), change it to127.0.0.1for local dev. Vite bakes these values in at start time, so restartnpm run devafter any change.
cd client
npm install
npm run devOpen http://localhost:3000 in your browser.
Open a second tab to test real-time multiplayer between two players.
# Stream Nakama logs (useful for debugging)
docker compose logs -f nakama
# Restart Nakama after editing index.js
docker compose restart nakama
# Stop everything
docker compose down
# Stop and delete all data (tiles, users, sessions)
docker compose down -v| Variable | Description | Local value | Production value |
|---|---|---|---|
VITE_NAKAMA_HOST |
Nakama hostname β bare host only, no https:// prefix |
127.0.0.1 |
your-app.onrender.com |
VITE_NAKAMA_PORT |
Nakama port | 7350 |
443 |
VITE_NAKAMA_SSL |
Use WSS / HTTPS | false |
true |
VITE_NAKAMA_KEY |
Server key (must match Nakama config) | defaultkey |
your secret key |
β οΈ Vite bakes these values into the JavaScript bundle at build time, not runtime. Changing them requires a rebuild (npm run build) or a Vercel redeploy.
β οΈ VITE_NAKAMA_HOSTmust be a bare hostname β no protocol, no trailing slash. Wrong:https://my-app.onrender.com/. Correct:my-app.onrender.com.
| Field | Default | Description |
|---|---|---|
socket.server_key |
defaultkey |
Must match VITE_NAKAMA_KEY |
console.username |
admin |
Admin console login |
console.password |
admin1234 |
Admin console password |
runtime.http_key |
defaulthttpkey |
HTTP RPC key |
The recommended production stack is:
| Layer | Service | Cost |
|---|---|---|
| Database | CockroachDB Serverless | Free tier (5 GB) |
| Backend | Render (Docker) | Free tier (spins down after inactivity) |
| Frontend | Vercel | Free tier |
Nakama needs a PostgreSQL-compatible database. CockroachDB Serverless has a permanent free tier.
-
Go to https://cockroachlabs.com/free and create an account.
-
Create a new Serverless cluster. Pick any region close to your Render region (e.g.
us-east-1). -
Once the cluster is ready, click Connect β Connection string.
Select General connection string. It looks like:postgresql://username:password@free-tier14.aws-us-east-1.cockroachlabs.cloud:26257/defaultdb?sslmode=verify-full -
Copy this string. You will need it in the next step.
Rename the database fromdefaultdbtonakamain the URL:postgresql://username:password@free-tier14.aws-us-east-1.cockroachlabs.cloud:26257/nakama?sslmode=verify-full
Render runs the Nakama game server as a Docker container.
Render deploys from a GitHub repo.
git remote add origin https://github.com/yourname/pixel-war-game.git
git push -u origin main-
Go to https://render.com β New β Web Service.
-
Connect your GitHub repo.
-
Configure the service:
Setting Value Name pixel-war-backend(or anything)Root Directory nakamaDockerfile Path ./DockerfileInstance Type Free Port 7350 -
Under Environment Variables, add:
Key Value DATABASE_ADDRESSyour CockroachDB connection string from Step 1 NAKAMA_SERVER_KEYa long random secret, e.g. pixelwar-secret-2025 -
Click Create Web Service.
Render will build the Docker image and start the server. First deploy takes ~3 minutes.
-
Once deployed, your backend URL will be something like:
https://pixel-war-backend.onrender.comTest it:
curl https://pixel-war-backend.onrender.com/healthcheck # expected: {}
Render's free tier spins down the server after 15 minutes of inactivity. The first request after spin-down takes ~30 seconds. If you need the server always on, upgrade to the $7/month Starter plan.
npm install -g vercelUpdate client/.env with your Render backend URL:
VITE_NAKAMA_HOST=pixel-war-backend.onrender.com
VITE_NAKAMA_PORT=443
VITE_NAKAMA_SSL=true
VITE_NAKAMA_KEY=pixelwar-secret-2025Use the bare hostname β no
https://, no trailing slash.
cd client
vercel deploy --prodFollow the prompts:
- Set up and deploy β
Y - Which scope β your Vercel account
- Link to existing project β
N(first time) - Project name β
pixel-war(or anything) - Directory β
.(current directory, i.e.client/)
Vercel will build and deploy. You get a URL like https://pixel-war.vercel.app.
-
Go to https://vercel.com β Add New Project.
-
Import your GitHub repo.
-
Set Root Directory to
client. -
Add environment variables:
Name Value VITE_NAKAMA_HOSTpixel-war-backend.onrender.comVITE_NAKAMA_PORT443VITE_NAKAMA_SSLtrueVITE_NAKAMA_KEYpixelwar-secret-2025 -
Click Deploy.
# After changing client code
cd client
vercel deploy --prod
# After changing index.js (server)
# Just push to GitHub β Render auto-deploys on push
git add nakama/data/modules/index.js
git commit -m "fix: update server logic"
git push| Environment | URL |
|---|---|
| Local | http://localhost:7351 |
| Render | Not directly exposed on free tier β use Render shell |
Local credentials (from nakama/local.yml):
- Username:
admin - Password:
admin1234
View saved tile state
Storage β Collection: pixel_war β Key: tiles β see the full JSON board.
See who is online
Matches β click pixel_war_global β see connected presences.
Reset the board
Storage β find the pixel_war / tiles record β delete it β restart Nakama.
All tiles go back to unclaimed.
Inspect a user
Users β search by username or user ID.
# ββ Local ββββββββββββββββββββββββββββββββββββββββββββββββββ
docker compose up -d # start backend
cd client && npm run dev # start frontend β localhost:3000
docker compose logs -f nakama # watch server logs
docker compose restart nakama # apply index.js changes
docker compose down -v # wipe everything
# ββ Deploy βββββββββββββββββββββββββββββββββββββββββββββββββ
# Backend: push to GitHub β Render auto-deploys
git push origin main
# Frontend: deploy to Vercel
cd client && vercel deploy --prod