Files
scrabble-game/gateway
Ilia Denisov f5c2404123
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 11s
CI / ui (pull_request) Successful in 32s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m7s
Stage 17 round 6 (#4/#5/#6): draft persistence wire + gateway + UI
Complete the client-side draft feature on top of the shipped backend
foundation (the game_drafts store/service):

- FB: DraftRequest{game_id,json} + DraftView{json} (a draft get reuses
  GameActionRequest); regenerated committed Go + TS bindings.
- Backend REST: GET/PUT /games/:id/draft, a draftDTO
  (rack_order/board_tiles) mapped to game.Draft.
- Gateway: draft.get/draft.save transcode forwarding the composition
  JSON verbatim (json.RawMessage both ways -- no double-encode).
- UI: debounced save of the rack order + board tiles and restore on
  load (lib/draft.ts), plus #5 -- tiles may be arranged on the
  opponent's turn (placement relaxed; the preview and Make-move stay
  your-turn-only, so an off-turn draft is position-only).

Tests: backend handler validation, gateway pass-through round-trip, UI
draft/codec units, and a draft-restore e2e.
2026-06-07 22:25:29 +02:00
..

gateway

The Scrabble platform's only public ingress (module scrabble/gateway). It terminates the client's Connect-RPC + FlatBuffers traffic over HTTP/2 cleartext (h2c), authenticates the originating credential, mints/resolves a thin opaque session, rate-limits, injects X-User-ID when forwarding to the backend over REST/JSON, and bridges the backend's gRPC push stream to each client's in-app live channel. It embeds the static UI build (go:embed, baked in by the gateway image's node stage) and serves the one SPA at / (web) and /telegram/ (the Mini App) — the single-origin model. It can also serve the backend's admin console at /_gm behind HTTP Basic-Auth for a local non-caddy run; in the deployed contour the front caddy owns /_gm (see ../deploy). See ../docs/ARCHITECTURE.md §2, §3, §10, §12, §13.

Package layout

cmd/gateway/            # main: config -> backend client -> session cache ->
                        #   push hub -> Connect h2c server (+ admin) -> serve
proto/edge/v1/          # Connect envelope contract (committed generated Go)
internal/config/        # GATEWAY_* env config
internal/backendclient/ # typed REST client (+ X-User-ID) and push gRPC client
internal/session/       # in-memory session cache (LRU/TTL, backend fallback)
internal/ratelimit/     # token-bucket limiter (golang.org/x/time/rate)
internal/connector/     # gRPC client to the Telegram connector (initData validate, out-of-app push) + routing
internal/push/          # live-event fan-out hub (per-user client streams)
internal/transcode/     # FlatBuffers<->REST bridge + message_type registry
internal/connectsrv/    # the Connect Gateway service over h2c (+ the in-memory active_users gauge)
internal/admin/         # Basic-Auth reverse proxy mounting the backend admin console at /_gm (verbatim)
internal/webui/         # embedded SPA build (go:embed dist) served at / and /telegram/

The FlatBuffers payloads and the backend push proto are the shared wire contracts in ../pkg.

Transport contract

A single Gateway Connect service: Execute(message_type, payload, request_id) for unary operations and Subscribe for the live stream. The payload bytes are FlatBuffers tables (scrabble/pkg/fbs); the gateway transcodes them to and from the backend's JSON. The session token rides in Authorization: Bearer; auth.* operations are unauthenticated and return the minted token. A unary domain outcome rides back in ExecuteResponse.result_code (HTTP 200); only edge failures become Connect error codes.

auth.telegram validates the Mini App initData by calling the Telegram connector (GATEWAY_CONNECTOR_ADDR), which holds the bot token; the gateway also routes out-of-app push to that connector for recipients with no live in-app stream (ARCHITECTURE.md §10). When GATEWAY_CONNECTOR_ADDR is unset, both are disabled.

The Stage 6 message-type slice: auth.telegram, auth.guest, auth.email.request, auth.email.login, profile.get, game.submit_play, game.state, lobby.enqueue, lobby.poll, chat.post; live events your_turn, opponent_moved, chat_message, nudge, match_found. Stage 7 added the play-loop ops; Stage 8 added the social/account/history ops — friends.* (list/incoming/request/respond/cancel/unfriend/code.issue/code.redeem), blocks.*, invitation.* (list/create/accept/decline/cancel), profile.update, stats.get, game.gcg, and the notify live event — all via the identical transcode pattern (transcode_social.go). Stage 11 added account linking & merge — link.email.request/confirm/merge and link.telegram.confirm/merge (transcode_link.go); the telegram ops validate the Login Widget payload via the connector (ValidateLoginWidget) and forward the trusted external_id. These superseded the Stage 8 email.bind.* ops, which were removed.

Configuration

Variable Default Notes
GATEWAY_HTTP_ADDR :8081 public Connect/h2c listener (also serves the admin console at /_gm)
GATEWAY_LOG_LEVEL info zap level
GATEWAY_BACKEND_HTTP_URL http://localhost:8080 backend REST base URL
GATEWAY_BACKEND_GRPC_ADDR localhost:9090 backend push gRPC address
GATEWAY_BACKEND_TIMEOUT 5s per backend REST call
GATEWAY_ADMIN_USER / GATEWAY_ADMIN_PASSWORD unset enable + guard the admin console at /_gm
GATEWAY_CONNECTOR_ADDR unset Telegram connector gRPC address (enables initData validation + out-of-app push)
GATEWAY_DEFAULT_SUPPORTED_LANGUAGES en,ru New Game variant gating set placed on the Session for non-platform logins (web / email / guest); a deployment may narrow it
GATEWAY_SESSION_TTL 10m cached session lifetime
GATEWAY_SESSION_CACHE_MAX 50000 cached session cap
GATEWAY_PUSH_HEARTBEAT_INTERVAL 15s live-stream keep-alive
GATEWAY_SERVICE_NAME scrabble-gateway OpenTelemetry service.name
GATEWAY_OTEL_TRACES_EXPORTER none none, stdout or otlp (gRPC; endpoint from OTEL_EXPORTER_OTLP_*)
GATEWAY_OTEL_METRICS_EXPORTER none none, stdout or otlp

Rate-limit defaults (built-in): public 30/min·IP (burst 10), authenticated 120/min·user (burst 40), admin 60/min·IP (burst 20), email-code 5/10 min·IP.

Run

GATEWAY_BACKEND_HTTP_URL=http://localhost:8080 \
GATEWAY_BACKEND_GRPC_ADDR=localhost:9090 \
go run ./gateway/cmd/gateway   # Connect edge on :8081

Generated code

The Connect envelope Go is committed under proto/edge/v1. Regenerate after editing the .proto (dev-time, like backend/cmd/jetgen):

make -C gateway tools   # go install protoc-gen-go + protoc-gen-connect-go
make -C gateway gen     # buf generate (local plugins)

The FlatBuffers payloads are generated in ../pkg (make -C pkg fbs).

Tests

go test -count=1 ./gateway/...

All gateway tests are hermetic: no real network, a fake backend (httptest) and credential fixtures. There is no integration (Docker) suite — the gateway holds no database.