Stage 1: backend foundation (Postgres, sessions, accounts, OTel)
Tests · Go / test (push) Successful in 11s
Tests · Integration / integration (push) Successful in 8s

- 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.
This commit is contained in:
Ilia Denisov
2026-06-02 13:52:26 +02:00
parent da079b2bc6
commit eeaad62b10
45 changed files with 3461 additions and 92 deletions
+34 -14
View File
@@ -71,8 +71,11 @@ arrive from a platform rather than completing a mandatory registration).
(`session_id`).
- The client holds `session_id` in memory for the app session (browser/OS
storage is optional and may be unavailable; losing it means re-login).
- The gateway caches `session → user_id` and injects `X-User-ID`. Sessions are
revocable. Session records live in `backend`.
- The gateway caches `session → user_id` and injects `X-User-ID`. Session
records live in `backend`, which stores only a **SHA-256 hash** of the opaque
token (never the plaintext), keeps a warmed in-memory cache for fast
resolution, and treats sessions as **revoke-only** — they have no TTL and live
until explicitly revoked (`status``revoked`).
- **Guest** = ephemeral web session (no platform, no email): session-only,
nothing persisted; restricted to auto-match, with no friends and no
stats/history. Platform users are auto-provisioned **durable** accounts.
@@ -82,6 +85,10 @@ arrive from a platform rather than completing a mandatory registration).
- One internal account may carry several **platform identities**
(`telegram`, `vk`, …) plus an optional **email** identity. First contact from
a platform auto-provisions a durable account bound to that platform identity.
Concretely, platform and email identities share one `identities` table keyed by
a unique `(kind, external_id)`; email is an identity with `kind=email` and a
`confirmed` flag (the confirm-code flow lands later). Accounts and identities
use application-generated **UUIDv7** primary keys.
- **Linking** is initiated from an authenticated profile: choose a platform →
complete that platform's web-auth confirm → attach the identity to the
current account.
@@ -155,9 +162,15 @@ within 10 seconds. Designed to be indistinguishable from a person.
## 9. Persistence
- Single Postgres database, schema `backend`; `backend` is the only writer.
pgx pool; queries via go-jet *(introduced when the first real query lands)*;
migrations embedded and applied with `pressly/goose/v3` at startup *(planned)*.
- Single Postgres database, schema `backend`; `backend` is the only writer. The
"pgx pool" is a `database/sql` handle backed by the pgx stdlib driver and
instrumented with otelsql; type-safe queries use **go-jet** (code generated
into `internal/postgres/jet` and committed, regenerated by `cmd/jetgen`).
Migrations are embedded SQL applied with `pressly/goose/v3` at startup. Primary
keys are application-generated **UUIDv7**.
- Stage 1 tables: `accounts` (durable internal accounts), `identities`
(platform/email identities, unique `(kind, external_id)`) and `sessions`
(revoke-only opaque-token hashes).
- **Active game state** is stored structurally with the `dict_version` pinned.
- **Statistics** (computed on finish): wins, losses, max points in a game, max
points for a single word.
@@ -185,12 +198,17 @@ delivery fans out to the appropriate channel.
## 11. Observability
- Structured logging with `go.uber.org/zap` (JSON). OpenTelemetry traces and
metrics, with a Prometheus pull endpoint where configured *(introduced with
the first real workload)*.
- Per-request server-side timing via middleware from day one. A client-measured
RTT piggybacked on the next request is a later enhancement, not MVP.
- Unauthenticated `GET /healthz` (liveness) and `GET /readyz` (readiness).
- Structured logging with `go.uber.org/zap` (JSON). OpenTelemetry tracer and
meter providers are wired (Stage 1), env-gated by
`BACKEND_OTEL_{TRACES,METRICS}_EXPORTER` with a default of `none` (so no
collector is required locally or in CI); `stdout` is available for debugging
and the Postgres pool is instrumented with otelsql. OTLP export, a Prometheus
pull endpoint, and dashboards arrive with the first real workload.
- Per-request server-side timing via gin middleware from day one (the access log
carries method, route, status, latency and the active trace id). A
client-measured RTT piggybacked on the next request is a later enhancement.
- Unauthenticated `GET /healthz` (liveness) and `GET /readyz` (readiness — the
database answers a bounded ping and the session cache is warmed).
## 12. Security boundaries
@@ -219,8 +237,10 @@ is something to deploy.
- Trunk is **`master`**; feature work happens on `feature/*` branches merged via
PR with a green CI gate (from Stage 1 onward — the genesis commit necessarily
lands on `master`).
- `.gitea/workflows/` holds the CI. `go-unit.yaml` runs gofmt/vet/build/test on
Go changes; more workflows (ui-test, integration, deploy) are added with the
components they cover.
- `.gitea/workflows/` holds the CI. `go-unit.yaml` runs gofmt/vet/build/unit-test
on Go changes; `integration.yaml` runs the Postgres-backed tests behind the
`integration` build tag (testcontainers `postgres:17-alpine`, Ryuk disabled,
serial). Further workflows (ui-test, deploy) are added with the components they
cover.
- After any push, the run is watched to green before a stage is declared done
(`python3 ~/.claude/bin/gitea-ci-watch.py`).