Files
galaxy-game/tools/local-dev
Ilia Denisov e63748c344 local-dev: boot-time dev sandbox provisions a runnable game on up
Adds backend/internal/devsandbox: an idempotent boot-time hook that,
when BACKEND_DEV_SANDBOX_EMAIL is set, ensures (1) the configured
engine_version row, (2) the real dev user, (3) PlayerCount-1
deterministic dummy users, (4) a private "Dev Sandbox" game with a
year-out turn schedule, (5) memberships for every participant via
the new lobby.Service.InsertMembershipDirect helper, (6) a drive of
the lifecycle to running. Re-running on a populated DB is a no-op;
partial states from earlier crashes are recovered.

tools/local-dev gains the matching env vars in .env, surfaces them
in compose, and acquires a `make build-engine` target that builds
galaxy-engine:local-dev from game/Dockerfile (a prerequisite of
`up`/`rebuild`). The compose game-state mount is changed from a
named volume to a host bind on /tmp/galaxy-game-state so backend's
bind-mount source for spawned engine containers resolves on the
docker daemon.

After `make -C tools/local-dev up`, login as dev@local.test with
the dev code 123456 and the Dev Sandbox already shows up in My
Games. Per-user behaviour for the same email survives a backend
restart.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 15:51:09 +02:00
..

tools/local-dev/ — Galaxy local development stack

A docker-compose stack that brings up postgres + redis + mailpit + backend + gateway so the UI Vite dev server (run on the host) can talk to a real authenticated stack without any cloud dependency.

The stack is the recommended baseline for UI work that goes beyond the mocked Playwright fixtures: every payload exercises the real FlatBuffers wire, every authenticated call verifies the response signature against the dev keypair, and every email passes through Mailpit's web UI for inspection.

This stack is not a CI gate (that role belongs to tools/local-ci/, which boots a Gitea + Actions runner and replays workflow files). The two stacks are independent and can coexist on the same machine; they bind different ports and use different networks.

Bring it up

make -C tools/local-dev up

up builds the local-dev backend and gateway images on first run (pulls postgres, redis, mailpit), waits for every service to report healthy, and returns. Subsequent invocations reuse the built images.

After the stack is healthy:

pnpm -C ui/frontend dev

Open http://localhost:5173 for the UI and http://localhost:8025 for Mailpit.

The first make up builds the engine image (galaxy-engine:local-dev) from game/Dockerfile. Subsequent invocations skip the build when the image already exists; force a rebuild with docker rmi galaxy-engine:local-dev followed by make build-engine.

Daily flow

make -C tools/local-dev up        # bring up (idempotent, fast on warm cache)
pnpm -C ui/frontend dev           # in another terminal
# ...edit UI, browse, repeat...
make -C tools/local-dev down      # stop containers, keep state

State persists in named Docker volumes between up/down cycles, so games created on Tuesday survive into Wednesday. Wipe with make clean when you want a fresh database.

Logging in

Two paths coexist by default:

  1. Fixed dev code (fast). tools/local-dev/.env ships BACKEND_AUTH_DEV_FIXED_CODE=123456. After requesting a code in the UI, type 123456ConfirmEmailCode accepts that literal in addition to the real bcrypt-verified code stored on the challenge row. The override emits a loud warning at backend boot and is rejected by the production env loader (BACKEND_ENV guard in backend/internal/config).
  2. Real Mailpit code. Open http://localhost:8025, find the most recent message, copy the six-digit code, paste it into the UI. This exercises the full mail outbox path, including SMTP handoff and gomail TLS-mode handling.

To force the second path (no fast-bypass), edit tools/local-dev/.env and clear BACKEND_AUTH_DEV_FIXED_CODE, then make rebuild (or simply docker compose up -d backend to recreate the backend with the new env).

Auto-provisioned dev sandbox

make up provisions a private game called Dev Sandbox owned by the dev user (default dev@local.test). The flow is implemented in backend/internal/devsandbox and runs on every backend boot when BACKEND_DEV_SANDBOX_EMAIL is non-empty in tools/local-dev/.env.

Bootstrap is idempotent — re-running make up after a make down finds the existing user, dummy participants, game, and memberships without creating duplicates. If a previous boot crashed mid-way (game stuck in enrollment_open or ready_to_start), the next boot resumes the lifecycle.

To log in straight into the sandbox:

  1. make -C tools/local-dev up
  2. pnpm -C ui/frontend dev (in another terminal)
  3. Open http://localhost:5173/login, enter dev@local.test, then the dev code 123456.
  4. The lobby shows Dev Sandbox in My Games; click in.

To disable the bootstrap, clear BACKEND_DEV_SANDBOX_EMAIL in tools/local-dev/.env and docker compose up -d backend (or make rebuild). Existing users / games are not removed.

The bootstrap requires:

  • galaxy-engine:local-dev Docker image (make build-engine).
  • BACKEND_DEV_SANDBOX_ENGINE_VERSION parses as plain semver (MAJOR.MINOR.PATCH); the default 0.1.0 is what the bootstrap registers in the engine_versions row that points at the image.
  • BACKEND_DEV_SANDBOX_PLAYER_COUNT ≥ 20 (the engine's minimum; 19 deterministic dummies fill the slots so the single real user can start the game).
  • A frozen turn schedule (0 0 1 1 * — once a year) so the visible game state stays at turn 1 until you explicitly progress it.

Network map

host                                            compose network "galaxy-local-dev-net"
  ┌────────────────────────────────┐              ┌──────────────────────────────┐
  │ browser    localhost:5173      │── pnpm dev (Vite, host) ──┐                  │
  │           ↳ /api/*  proxied ───┼──────────────────────────▶│ gateway:8080     │
  │           ↳ /galaxy.gateway... ┼──────────────────────────▶│                  │
  │ browser    localhost:8025      │─────────────────────────▶│ mailpit:8025      │
  │ psql       localhost:5433      │─────────────────────────▶│ postgres:5432     │
  │ redis-cli  localhost:6380      │─────────────────────────▶│ redis:6379        │
  └────────────────────────────────┘              │  ↳ backend:8080  (HTTP)        │
                                                  │  ↳ backend:8081  (gRPC push)   │
                                                  │  ↳ mailpit:1025  (SMTP in)     │
                                                  └────────────────────────────────┘

Vite's dev server proxies /api and /galaxy.gateway.v1.EdgeGateway to the gateway, so every browser request stays same-origin (no CORS preflight). The gateway is therefore reachable only through Vite at http://localhost:5173, not at http://localhost:8080 from the browser tab. Direct curl/wget against http://localhost:8080 still works for diagnostic probes — only the browser-side requests are proxied.

Mailpit (8025), postgres (5433), and redis (6380) remain directly reachable for diagnostics (make psql, redis-cli -h localhost -p 6380 -a galaxy-dev).

To point the proxy at a non-local gateway, run VITE_DEV_PROXY_TARGET=http://gateway.host:8080 pnpm -C ui/frontend dev — no compose changes needed.

Make targets

make up             Bring up the stack (build engine + compose images if needed) and wait for health
make rebuild        Rebuild the backend / gateway images (ignores cache)
make build-engine   Build galaxy-engine:local-dev from game/Dockerfile (no-op if image already present)
make down           Stop containers, keep volumes
make clean          Stop and wipe volumes (postgres + game-state)
make logs           Tail every service's logs
make logs-backend   Tail backend only
make logs-gateway   Tail gateway only
make logs-mail      Tail mailpit only
make psql           Open a psql shell as galaxy@galaxy_backend
make status         docker compose ps

Files

  • docker-compose.yml — five services: postgres, redis, mailpit, backend, gateway, plus shared network and volumes.
  • backend.Dockerfile, gateway.Dockerfile — local-dev runtime images built on alpine (so wget is available for the compose healthchecks). The build stage mirrors backend/Dockerfile and gateway/Dockerfile exactly.
  • Makefile — wrapper over docker compose that keeps the muscle memory close to tools/local-ci/'s Makefile.
  • .env — committed defaults for the compose ${VAR:-} expansions. Edit per-developer or override via your shell.
  • keys/gateway-response.pem, keys/gateway-response.pub — dev-only Ed25519 keypair used by the gateway for response signing. Pairs with the VITE_GATEWAY_RESPONSE_PUBLIC_KEY value in ui/frontend/.env.development. See keys/README.md before rotating.
  • keys/regenerate.go — one-shot Go helper that regenerates the pair and prints the new base64 public key.

Troubleshooting

  • make up reports a build error mentioning pkg/cronutil — upstream module list drifted; copy any new pkg/<name>/ line into the local-dev backend.Dockerfile / gateway.Dockerfile to match backend/Dockerfile / gateway/Dockerfile.
  • Gateway exits at boot with "redis: …" — the redis container is still bootstrapping. make up --wait waits for healthchecks; if it times out, increase start_period in the gateway service or inspect make logs-redis.
  • Login form rejects every code — confirm BACKEND_AUTH_DEV_FIXED_CODE is set in tools/local-dev/.env and the backend has been recreated since the last edit (docker compose up -d backend). Real Mailpit codes work regardless.
  • UI talks to old gateway: Vite caches import.meta.env at boot. Restart pnpm dev after editing ui/frontend/.env.development.local.
  • Port 8080 already in use — stop the conflicting service or edit the host-side mapping in docker-compose.yml (gateway's ports: entry) plus the matching VITE_GATEWAY_BASE_URL in ui/frontend/.env.development.local.

Relationship to other infrastructure

  • tools/local-ci/ — Gitea + Actions runner, replays .gitea/workflows/* against a pushed branch. Different stack, different purpose; coexists with local-dev on the same machine.
  • integration/testenv/ — testcontainers harness used by make -C integration integration. Uses the same images (backend/Dockerfile, gateway/Dockerfile) at production defaults; do not confuse with this local-dev stack, which carries alpine-runtime images for ergonomics and the dev-mode auth override.