Replace the dictionary hot-reload with an online update flow in the GM console (/_gm/dictionary): the operator uploads scrabble-dawg-vX.Y.Z.tar.gz, previews the per-variant words added/removed against the active dictionary, and confirms to install + activate it. Versions are immutable; in-progress games keep their pinned version while new games use the new one. - engine: DiffWords (enumerate both DAWGs, decode only the differences), OpenFinder, Registry.Finder, DictFiles; OpenWithVersions skips the .staging area. - dictadmin: hardened release-archive validation + extraction (path-traversal, symlink, oversize, entry-count rejection) and staging -> install (atomic rename). - game: active dictionary version persisted in the dictionary_state singleton (single source of truth, restored on boot), concurrency-safe accessor. - storage: BACKEND_DICT_DIR is a named volume seeded from the image (nonroot-owned), so uploaded versions persist across redeploys; the build's DICT_VERSION labels the seed and equals the resident tag (BACKEND_DICT_VERSION). - docs: ARCHITECTURE §5, FUNCTIONAL (+ru), backend README, TESTING, PRERELEASE. Tests: engine + dictadmin unit; integration upload->preview->install->activate-> restart->pin->immutability->CSRF.
scrabble-game
Multiplatform Scrabble game. Players arrive from a platform (Telegram first; later VK/MAX/iOS/Android) or from standalone web (email / guest). The game supports English Scrabble, Russian Scrabble and Эрудит.
Components
gateway— the only public ingress: anti-abuse, platform authentication (resolves the player and injectsX-User-ID), routing tobackend, and an admin surface behind Basic Auth.backend— internal-only service that owns every domain concern and embeds thescrabble-solverengine library in-process.ui— pure-HTML5 client (plain Svelte 5 + TypeScript + Vite) over Connect-RPC- FlatBuffers, embeddable in platform webviews and packageable to native via
Capacitor. See
ui/README.md.
- FlatBuffers, embeddable in platform webviews and packageable to native via
Capacitor. See
platform/*— per-platform side-services (e.g. the Telegram bot).
Documentation (sources of truth)
docs/ARCHITECTURE.md— global architecture, transport, security, cross-service contracts.docs/FUNCTIONAL.md(+_ru) — per-domain user stories.docs/TESTING.md— test layers and the per-stage CI gate.PLAN.md— the staged implementation plan and stage tracker.CLAUDE.md— project guide and the mandatory per-stage workflow.
Build & test
go build ./backend/... ./pkg/... ./gateway/... # per module (the workspace spans several)
go vet ./backend/... ./pkg/... ./gateway/...
gofmt -l . # must print nothing
go test -count=1 ./backend/... ./pkg/... ./gateway/... # unit tests
go test -tags=integration -count=1 -p=1 ./backend/... # + Postgres (needs Docker)
The integration-tagged tests start a throwaway postgres:17-alpine container
via testcontainers-go and require a reachable Docker daemon; they live in the
backend module. The wire contracts in pkg and the Connect edge in gateway
have committed generated code (regenerate dev-time with make -C pkg gen /
make -C gateway gen).
Run the backend locally
The backend now owns persistence, so it needs Postgres and applies its embedded migrations at startup:
docker run -d --name scrabble-pg -e POSTGRES_PASSWORD=dev -p 5432:5432 postgres:17-alpine
BACKEND_POSTGRES_DSN='postgres://postgres:dev@localhost:5432/postgres?search_path=backend&sslmode=disable' \
go run ./backend/cmd/backend # HTTP API + probes on :8080, push gRPC on :9090
Run the gateway locally
The gateway is the public edge; point it at a running backend:
GATEWAY_BACKEND_HTTP_URL=http://localhost:8080 \
GATEWAY_BACKEND_GRPC_ADDR=localhost:9090 \
go run ./gateway/cmd/gateway # Connect/h2c edge on :8081
Key environment: BACKEND_HTTP_ADDR (default :8080), BACKEND_LOG_LEVEL
(debug|info|warn|error, default info), BACKEND_POSTGRES_DSN (required).
The full configuration surface and the go-jet regeneration step live in
backend/README.md.
Run the UI locally
cd ui && pnpm install
pnpm start # mock mode: lobby -> game with no backend, on http://localhost:5173
pnpm dev # against a running gateway (Vite proxies the RPC path to :8081)
pnpm check (type-check), pnpm test:unit (Vitest), pnpm test:e2e (Playwright
smoke vs the mock), pnpm build (static bundle). Details — including the committed
edge codegen (pnpm codegen) — are in ui/README.md.
Deploy (deploy/)
The full contour is deploy/docker-compose.yml:
backend + gateway (with the UI embedded via go:embed, baked in by its node
build stage) + Postgres + the Telegram connector (with a VPN sidecar) + an
observability stack (OTel Collector → Prometheus + Tempo → Grafana) + a front
caddy that owns a single /_gm Basic-Auth (admin console + Grafana). The Go
services build from multi-stage distroless */Dockerfile.
docker build -f backend/Dockerfile -t scrabble-backend . # pulls the DAWG release artifact
docker build -f gateway/Dockerfile -t scrabble-gateway . # node stage builds + embeds the UI
docker compose -f deploy/docker-compose.yml config # validate (needs the TEST_/PROD_ env)
CI auto-deploys the test contour on a PR into — or push to — development
(.gitea/workflows/ci.yaml); the prod contour is a manual deploy after
development → master. Env reference: deploy/.env.example;
the topology and the two-contour model are in
docs/ARCHITECTURE.md §13.