eeaad62b10
- 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.
61 lines
2.8 KiB
SQL
61 lines
2.8 KiB
SQL
-- +goose Up
|
|
-- Initial schema for the Scrabble backend service: durable accounts, their
|
|
-- platform/email identities, and opaque server sessions.
|
|
--
|
|
-- Every backend table lives in the `backend` schema. The schema is created here
|
|
-- so a fresh database can apply this migration, and search_path is pinned for
|
|
-- the rest of the migration so the CREATE statements land in `backend` without
|
|
-- qualifying every object. Production also pins search_path via
|
|
-- BACKEND_POSTGRES_DSN.
|
|
CREATE SCHEMA IF NOT EXISTS backend;
|
|
SET search_path = backend, pg_catalog;
|
|
|
|
-- Durable internal accounts. Guests are session-only and never reach this table.
|
|
CREATE TABLE accounts (
|
|
account_id uuid PRIMARY KEY,
|
|
display_name text NOT NULL DEFAULT '',
|
|
preferred_language text NOT NULL DEFAULT 'en',
|
|
time_zone text NOT NULL DEFAULT 'UTC',
|
|
block_chat boolean NOT NULL DEFAULT false,
|
|
block_friend_requests boolean NOT NULL DEFAULT false,
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
CONSTRAINT accounts_preferred_language_chk CHECK (preferred_language IN ('en', 'ru'))
|
|
);
|
|
|
|
-- Platform and email identities attached to an account. external_id is the
|
|
-- platform user id (kind='telegram') or the email address (kind='email');
|
|
-- confirmed flips true once an email confirm-code is verified (later stages).
|
|
CREATE TABLE identities (
|
|
identity_id uuid PRIMARY KEY,
|
|
account_id uuid NOT NULL REFERENCES accounts (account_id) ON DELETE CASCADE,
|
|
kind text NOT NULL,
|
|
external_id text NOT NULL,
|
|
confirmed boolean NOT NULL DEFAULT false,
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
CONSTRAINT identities_kind_chk CHECK (kind IN ('telegram', 'email')),
|
|
CONSTRAINT identities_kind_external_id_key UNIQUE (kind, external_id)
|
|
);
|
|
CREATE INDEX identities_account_idx ON identities (account_id);
|
|
|
|
-- Opaque server sessions. token_hash is the hex-encoded SHA-256 of the bearer
|
|
-- token; the plaintext token is never stored. Sessions are revoke-only (no
|
|
-- TTL): status moves active -> revoked and revoked_at is stamped.
|
|
CREATE TABLE sessions (
|
|
session_id uuid PRIMARY KEY,
|
|
account_id uuid NOT NULL REFERENCES accounts (account_id) ON DELETE CASCADE,
|
|
token_hash text NOT NULL,
|
|
status text NOT NULL DEFAULT 'active',
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
last_seen_at timestamptz,
|
|
revoked_at timestamptz,
|
|
CONSTRAINT sessions_status_chk CHECK (status IN ('active', 'revoked')),
|
|
CONSTRAINT sessions_token_hash_key UNIQUE (token_hash)
|
|
);
|
|
CREATE INDEX sessions_account_idx ON sessions (account_id);
|
|
|
|
-- +goose Down
|
|
DROP TABLE sessions;
|
|
DROP TABLE identities;
|
|
DROP TABLE accounts;
|