ADR-0016 · Ингест браузерных партий через play-api

Статус: принято · 2026-08-01 · заменяет ADR-0005

Контекст. С фазы 1 браузерные партии против встроенных ботов шли в аналитику через отдельный токен-шлюз на Koyeb (ADR-0005, ~350 строк Node). К августу 2026 play-api уже владеет превосходящим механизмом доставки — транзакционный outbox + IngestDeliverer (backoff 5с→5мин, парковка 4xx, идемпотентные UUIDv5) — но использует его только для партий, сыгранных на самом сервере. Итого: три реализации одного wire-контракта (маппер SPA, валидатор шлюза, PlaysiteIngest), три платформы деплоя, а браузерный путь — слабейший по надёжности (ретрай только при следующем визите на /play, без backoff; партия теряется навсегда, если пользователь чистит IndexedDB). Плюс стратегический риск: Koyeb куплен Mistral (02-2026), free-tier закрыт для новых аккаунтов — наш слот grandfathered, срок жизни неизвестен. Ещё ADR-0005 предвидел: «с фазы 3 ингест уезжает на серверное ядро — браузерное реле уходит».

Решение. play-api принимает браузерные отчёты сам: публичный POST /ingest/games (без аутентификации — токен в браузере не появляется, инвариант ADR-0005 сохранён). Защиты шлюза перенесены и выполняются от дешёвой к дорогой: per-IP rate-limit (60/мин) до чтения тела → потоковый лимит 256 KB → структурная валидация (порт validate.ts; source прибит к playsite, id ужесточён до UUID). Принятое ложится в отдельную таблицу client_reports (зеркало outbox без FK — у браузерной партии нет снапшота в games), которую дренирует второй экземпляр того же IngestDeliverer. Новых env-переменных нет — INGEST_URL/INGEST_TOKEN обслуживают обе очереди.

Ключевое проектное ограничение. Trust-граница переезжает внутрь сервиса: «что сервер сыграл» (outbox, доверенное, транзакционно) и «что браузер рассказал» (client_reports, подделываемое, провалидировано только структурно) — разные таблицы и разные write-пути. Клиентские отчёты идут только в relay-очередь и никогда — в game_results/game_archive//history/лидерборды. Авторитетный валидатор — по-прежнему движковый реплей в аналитике.

Последствия.

  • Контракт статусов для SPA сохранён (201/200/400/422/413/429, classify() не менялся), но приём стал асинхронным: 201 = «принято в очередь», реджект реплей-гейта паркует строку сервер-side (failed_permanently + last_error) и клиенту больше не виден; браузерный карантин покрывает только структурные реджекты.
  • Durability выросла: с момента 201 доставку гарантирует серверный outbox с backoff, а не «повезёт при следующем визите».
  • VITE_INGEST_GATEWAY_URL удалена; запись включается тем же VITE_PLAY_API_URL, что и live-поверхность.
  • Вывод из эксплуатации: шлюз на Koyeb живёт окно наблюдения ~1–2 недели (стухшие бандлы SPA и старые IndexedDB-outbox дольются), затем сервис удаляется, репо архивируется. Освободившийся free-слот Koyeb (0.1 vCPU / 512 MB, scale-to-zero) годится под лёгкого бота каталога — но платформа после покупки Mistral временная, не закладываться.
  • Реализация: dicechess-play-api#213 (эндпоинт + V11 client_reports + второй деливерер), dicechess-play#184 (клиент). Порядок раскатки жёсткий: сначала релиз play-api в прод, потом мердж клиента.

Альтернативы (отклонены).

  • Оставить шлюз, переписать в Cloudflare Worker — убирает Koyeb, но сохраняет третью реализацию контракта и слабый браузерный ретрай.
  • Публичный no-token lane прямо в аналитике — выставляет aurora (homelab) напрямую браузерному трафику; сознательно избегали с ADR-0005.
  • Снять FK и класть отчёты в общий outbox — меньше кода, но семантическая перегрузка game_id и потеря структурной trust-границы; отвергнуто в пользу отдельной таблицы.

🔗 ADR-0005-Shlyuz-ingesta-na-Koyeb · 02-Arkhitektura-avtoritet-i-stek · 03-Faza-1-anonim-vs-bot-ingest