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(эндпоинт + V11client_reports+ второй деливерер),dicechess-play#184(клиент). Порядок раскатки жёсткий: сначала релиз play-api в прод, потом мердж клиента.
Альтернативы (отклонены).
- Оставить шлюз, переписать в Cloudflare Worker — убирает Koyeb, но сохраняет третью реализацию контракта и слабый браузерный ретрай.
- Публичный no-token lane прямо в аналитике — выставляет aurora (homelab) напрямую браузерному трафику; сознательно избегали с ADR-0005.
- Снять FK и класть отчёты в общий outbox — меньше кода, но семантическая перегрузка
game_idи потеря структурной trust-границы; отвергнуто в пользу отдельной таблицы.