New platform/telegram connector (own container, bot token only there): - go-telegram/bot long-poll loop: /start deep-links + Mini App launch button. - gRPC API pkg/proto/telegram/v1 (Telegram service): ValidateInitData, Notify (renders a localized message + deep-link button), SendToUser/SendToGameChannel (admin, wired in Stage 10). Generic methods are platform-agnostic (external_id). - Bot API base override for Telegram's test environment; Dockerfile + compose (VPN sidecar, no public ingress); README. Gateway: - initData validation relocated from the gateway into the connector; the gateway calls ValidateInitData over gRPC (GATEWAY_CONNECTOR_ADDR), drops the bot token, and deletes internal/auth. - Out-of-app push: runPushPump routes events whose recipient has no live in-app stream to connector.Notify, gated by /internal/push-target + the in-app-only flag (race-free de-dup); HasSubscribers added to the push hub. Backend: - Migration 00007 accounts.notifications_in_app_only (default true) + jetgen. - ProvisionTelegram seeds a new account's language/display name from the launch fields; IdentityExternalID reverse lookup; /internal/push-target handler. UI: - Telegram Mini App launch: detect initData, apply themeParams, authTelegram, route the deep-link start_param (g/i/f); /telegram/ guard redirects outside Telegram. Vite relative base + telegram-web-app.js. In-app-only profile toggle; share-to-Telegram link for a friend code. Vitest + Playwright coverage. Wire/docs/CI: fbs Profile/UpdateProfileRequest gain notifications_in_app_only (Go + TS); go.work uses ./platform/telegram; go-unit.yaml covers it; PLAN, ARCHITECTURE, FUNCTIONAL (+ru), UI_DESIGN, READMEs updated.
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 also fronts the backend admin API behind HTTP
Basic-Auth. See ../docs/ARCHITECTURE.md §2, §3, §10,
§12.
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
internal/admin/ # Basic-Auth reverse proxy to the backend admin API
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,
email.bind.*, stats.get, game.gcg, and the notify live event — all via the
identical transcode pattern (transcode_social.go).
Configuration
| Variable | Default | Notes |
|---|---|---|
GATEWAY_HTTP_ADDR |
:8081 |
public Connect/h2c listener |
GATEWAY_ADMIN_ADDR |
:8082 |
admin proxy listener (enabled only with creds) |
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 proxy |
GATEWAY_TELEGRAM_BOT_TOKEN |
unset | enable the Telegram auth path |
GATEWAY_SESSION_TTL |
10m |
cached session lifetime |
GATEWAY_SESSION_CACHE_MAX |
50000 |
cached session cap |
GATEWAY_PUSH_HEARTBEAT_INTERVAL |
15s |
live-stream keep-alive |
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.