- internal/postgres: pgx-over-database/sql pool (otelsql), embedded goose
migrations into schema 'backend', committed go-jet code + cmd/jetgen tool.
- internal/account: durable accounts + unified telegram/email identities
(UUIDv7 keys), find-or-create provisioning with unique-conflict handling.
- internal/session: opaque 256-bit tokens stored as a SHA-256 hash, revoke-only
(no TTL); write-through cache gating /readyz; store + service.
- internal/telemetry: OTel tracer/meter providers (none/stdout) + request-timing
middleware; internal/config gains Postgres + OTel env loading.
- internal/server: /api/v1 {public,user,internal,admin} skeleton + X-User-ID
middleware; /readyz checks DB ping + cache; main wires
telemetry -> db+migrate -> warm cache -> server.
- Tests: unit + integration (build tag 'integration', testcontainers
postgres:17) for migrations, accounts, sessions, readyz; new integration.yaml.
- Docs: ARCHITECTURE, TESTING, PLAN refinements, root + backend READMEs.
Session/account REST handlers deferred to Stage 6 (gateway); OTLP + dashboards
to Stage 11.
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.
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
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
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.