Files
scrabble-game/gateway
Ilia Denisov 645df52c0b
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 8s
CI / integration (pull_request) Successful in 13s
CI / ui (pull_request) Successful in 32s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m8s
Round-6 follow-up: UX polish + client-IP fix
- Client IP: the compose caddy trusts X-Forwarded-For from private-range
  upstreams (trusted_proxies private_ranges), so the real client IP survives
  the host-caddy hop (it was logging the docker caddy hop 172.18.0.x for chat
  moderation and bucketing the gateway per-IP rate limiter on it). Correct and
  spoof-safe in both contours (prod has no host caddy); peerIP unit-tested.
- Ad banner gated off behind a compile-time SHOW_AD_BANNER=false (the if-branch,
  the AdBanner import and banner.ts are tree-shaken out of the prod bundle).
- Landing: the Telegram entry is just the 64px logo (clickable, no button/text).
- TG-fullscreen header: title + menu centred as a pair (hamburger right of the
  title), pinned to the bottom of the TG nav band.
- Edge-swipe back (Screen): a left-edge rightward drag navigates to back
  (touch/pen only, armed from <=24px; skipped inside Telegram).
- Chat soft-keyboard: a bottom-sheet Modal lifted above the keyboard by a
  visualViewport-driven transform (compositor-only, no page/sheet relayout).
  iOS-specific, needs on-device tuning; native resize=none awaits Capacitor.
- Tests: e2e for the in-game '✓ in friends' item and a board→board tile
  relocation; codec units for last_activity_unix + OutgoingRequestList.

Deferred to the next PR (agreed): #4 enrich the your-turn/game-end push; #5 hide
finished games from the lobby.
2026-06-08 21:31:44 +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 a landing page at / and the game SPA at /app/ (web) and /telegram/ (the Mini App) — the single-origin model. Hash-named /assets/* are served immutable; the HTML shells are no-cache. 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 UI build (go:embed dist): landing at /, SPA at /app/ + /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 10s live-stream keep-alive (an immediate heartbeat also fires on open, under the ~15s edge idle timeout)
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.