GeoDuels is a production-ready + dev-ready GeoGuessr-style platform built for horizontal scaling.
apps/web(Next.js): browser UI and gameplay shell.services/api(Go): auth/session/profile endpoints and backend API surface (/v1).services/match-coordinator(Go): matchmaking over websocket (/queue), assignment, maintenance status, and recovery (/v1/session/recover).services/realtime-gateway(Go): websocket gatewaying (/ws/{node}) to the assigned gameplay node.services/gameplay-node(Go): round engine and authoritative match state broadcast for assigned matches.workers/location-ingest(Go): one-off/cron worker that validates and ingests location datasets into PostgreSQL.
- PostgreSQL: source of truth for persistent data (profiles, stats, location catalog, match persistence).
- Redis: queue and distributed coordination state for matchmaking and gameplay node ownership.
- Dataset JSON files (
datasets/*.json): seed source for location ingest.
- Browser loads
web. - Browser calls
apifor auth + app APIs (/v1). - Browser opens websocket matchmaking to
match-coordinator(/queue) to enter duels. match-coordinatorassigns a match + gameplay route and issues ticket.- Browser upgrades to websocket through
realtime-gateway(/ws/{node}), which proxies to the assignedgameplay-node. gameplay-noderuns duel engine and broadcasts authoritative snapshots.
/is the lobby and launcher./match/[id]is the canonical route for a specific match.- Cold loads resolve through
GET /v1/matches/{id}/bootstrap. - Already-authenticated route refreshes can resolve through
GET /v1/matches/{id}/session. - A match route can resolve to:
- live reconnect with a minted gameplay ticket
- saved history / end-of-match snapshot
- replaced, forbidden, or missing state
/->web/v1->api/queueand/queue/online->match-coordinator/ws->realtime-gateway
Development (docker-compose.yml) uses language runtime images for fast iteration on the backend:
golang:1.26forapi,match-coordinator,gameplay-nodepostgres:16redis:7
The web app is typically run directly from apps/web with Node during local development.
Production images are built from service Dockerfiles and pushed to registry:
geoduels-apigeoduels-match-coordinatorgeoduels-realtime-gatewaygeoduels-gameplay-nodegeoduels-webgeoduels-location-ingest
gameplay-nodemarks itself draining on shutdown, refuses new match creation, and waits for active matches to finish before exit.match-coordinatorexcludes draining gameplay nodes from new duel assignment.realtime-gatewaystops accepting new websocket upgrades during shutdown and waits for active proxied sockets to close.api,match-coordinator,realtime-gateway, andgameplay-nodeall fail readiness while draining so Kubernetes can stop routing new traffic.- Redis key
system:maintenancecan publish lobby maintenance state:queuePaused: pause duel queueingplayPaused: pause all new play sessionsphase: warning|active: drive lobby warning banner / blocking maintenance overlay
Prerequisites:
- Docker Desktop
- Go 1.26+
- Node 20+
Start:
cp .env.example .env
docker compose up -d postgres redis gameplay-node match-coordinator realtime-gateway api
cd apps/web && npm ci && npm run devEndpoints:
- Web:
http://localhost:3000 - API health:
http://localhost:8080/health - Queue health:
http://localhost:8090/health - Gameplay health:
http://localhost:8091/health
Stop:
docker compose downTriggered by git tag push.
- Run Go, web, and manifest checks
- Build and push versioned production images
- Open a PR against the private ops repository configured by
OPS_REPOSITORY - Update production image tags and
NEXT_PUBLIC_APP_VERSIONin that ops repository - Deploy after that release PR is merged and Flux reconciles production
- Provision k3s cluster, ingress, DNS, and TLS.
- Create namespace and required secrets (
geoduels-secrets,ghcr-creds) in the private ops flow. - Apply DB migrations in
db/migrations. - Configure the release workflow variables/secrets, especially
OPS_REPO_TOKEN. - Push a release tag (for example
v1.2.3) to build images and open the Flux release PR. - Merge the generated release PR to trigger production rollout through Flux.
- Run post-deploy health checks for
/health, queue flow, and websocket gameplay.
docker-compose.yml- local stackinfra/k3s/base- base k8s manifestsinfra/k3s/overlays/k3d- local 3-node k3d overlay for routing/scaling tests- production overlays and Flux cluster state live in the private ops repository
infra/k3s/overlays/k3d- local 3-node k3d overlay for routing/scaling testsservices/*/Dockerfile,apps/web/Dockerfile,workers/location-ingest/Dockerfile- production image definitionsdocs/architecture.md- current runtime architecture