Files
scrabble-game/backend
Ilia Denisov 6d0dd4fb14
Tests · Go / test (push) Successful in 6s
Tests · Integration / integration (push) Successful in 7s
Stage 2: engine package over scrabble-solver (registry, bag, Game, replay)
backend/internal/engine wraps the sibling scrabble-solver library in-process:

- Registry: versioned DAWG load via dafsa.Load, keyed by (variant, dict_version),
  latest-per-variant; English / Russian / Эрудит handled uniformly.
- Bag: own deterministic, seeded tile bag with Draw + Return (for exchanges),
  since the solver's self-play bag cannot return tiles.
- Game: pure rules engine — deal, play/pass/exchange/resign, refill, per-move
  scoring, turn order, and end-condition detection (empty bag + empty rack, six
  scoreless turns, resignation) with end-game rack adjustment.
- decode/ReplayBoard: dictionary-independent MoveRecords and board replay via
  scrabble.Apply (no internal/encoding), realising ARCHITECTURE §9.1.

Wiring: go.work gains "replace scrabble-solver => ../scrabble-solver"; backend
requires scrabble-solver (placeholder) and github.com/iliadenisov/dafsa directly.
Both Go CI workflows clone the public solver sibling (master HEAD, no token) and
set BACKEND_DICT_DIR.

Docs: ARCHITECTURE §5/§14, TESTING engine layer, backend README, and PLAN
refinements + deferred TODOs (publish/version solver; split engine vs dictionary
generator).
2026-06-02 15:10:08 +02:00
..

backend

Internal-only domain service for the Scrabble platform (module scrabble/backend). It owns identity/sessions, accounts, and — in later stages — the lobby, game runtime, robot, chat, history and administration. Its only network consumers are the gateway and the platform side-services; it is never exposed publicly.

As of Stage 1 the backend provides the foundation: configuration, the HTTP listener with the /api/v1 route-group skeleton and probes, the Postgres pool with embedded goose migrations, OpenTelemetry wiring, an in-memory session cache, and the durable accounts / identities / sessions data model. The session and account REST endpoints are added with the gateway (Stage 6); Stage 1 ships the store/service layer they will call.

Stage 2 adds internal/engine, the in-process bridge to the scrabble-solver library: a versioned dictionary registry, a deterministic tile bag, and a pure rules Game (legal plays, passes, exchanges, resignations and end-condition detection) that emits dictionary-independent move records. It is a library only; the game domain wires it into the server in Stage 3.

Package layout

cmd/backend/         # process entrypoint: telemetry -> db+migrate -> cache -> server
cmd/jetgen/          # dev tool: regenerate go-jet code from a throwaway container
internal/config/     # env configuration (composes postgres + telemetry config)
internal/telemetry/  # OpenTelemetry providers + per-request timing middleware
internal/postgres/   # pgx-over-database/sql pool (otelsql), goose migrations
  migrations/        #   embedded *.sql (goose), schema `backend`
  jet/               #   generated go-jet models + table builders (committed)
internal/account/    # durable accounts + platform/email identities (store)
internal/session/    # opaque tokens, sessions store, write-through cache, service
internal/server/     # gin engine, route groups, X-User-ID middleware, probes
internal/engine/     # in-process scrabble-solver bridge: registry, bag, Game, replay

Configuration (environment)

Variable Default Notes
BACKEND_HTTP_ADDR :8080 HTTP listen address.
BACKEND_LOG_LEVEL info debug / info / warn / error.
BACKEND_POSTGRES_DSN Required. pgx/libpq URL; must pin search_path=backend.
BACKEND_POSTGRES_MAX_OPEN_CONNS 25 Pool max open connections.
BACKEND_POSTGRES_MAX_IDLE_CONNS 5 Pool max idle connections.
BACKEND_POSTGRES_CONN_MAX_LIFETIME 30m Max connection lifetime.
BACKEND_POSTGRES_OPERATION_TIMEOUT 5s Connect attempt + /readyz ping bound.
BACKEND_SERVICE_NAME scrabble-backend OpenTelemetry service.name.
BACKEND_OTEL_TRACES_EXPORTER none none or stdout (OTLP arrives later).
BACKEND_OTEL_METRICS_EXPORTER none none or stdout.

Run

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 ./cmd/backend

On boot the backend opens the pool, creates the backend schema if needed, and applies the embedded migrations. GET /healthz reports liveness; GET /readyz reports 200 only when the database answers and the session cache is warmed.

Migrations & generated code

Migrations are plain goose SQL under internal/postgres/migrations (sequential NNNNN_name.sql), embedded and applied at startup. After changing the schema, regenerate the committed go-jet code (needs Docker):

go run ./cmd/jetgen     # rewrites internal/postgres/jet against a temp container

Engine & dictionaries

internal/engine consumes the sibling scrabble-solver module in-process. Its bare module path (scrabble-solver, not a URL) cannot be fetched via VCS, so the workspace go.work carries replace scrabble-solver => ../scrabble-solver and the build must run from the repository root (the workspace), not from this module in isolation. github.com/iliadenisov/dafsa (the DAWG loader) is a direct dependency. CI clones the public solver repository into ../scrabble-solver before building (see .gitea/workflows/); locally, check it out next to this repository. Committed dictionaries (en_sowpods.dawg, ru_scrabble.dawg, ru_erudit.dawg) live in the solver's dawg/ directory; the engine loads them by (variant, dict_version) from a directory path. A configurable BACKEND_DICT_DIR is wired when the first consumer needs it (Stage 3); the future versioned-artifact direction is recorded in ../PLAN.md TODO-2.

Tests

go test -count=1 ./...                       # unit tests (no Docker)
go test -tags=integration -count=1 -p=1 ./... # Postgres-backed (needs Docker)

Integration tests are guarded by the integration build tag and run against a throwaway postgres:17-alpine container; they fail loudly when Docker is absent rather than skipping. The internal/engine tests load the committed DAWGs from BACKEND_DICT_DIR (defaulting to the sibling ../scrabble-solver/dawg) and fail loudly when that directory is absent.