Проект перехода: сравнение исходной схемы с целевой и порядок переключения. Гостевой HTML и отказы tRPC частично проверены 31.10.2026 — BFF сейчас. Вход, refresh и операции с данными остаются планом до проверки; канарейка — следующий этап.
Было и стало
Сейчас токены кладёт в cookie сам Go API, а браузер получает их напрямую:
flowchart LR
B["Браузер"] -->|"cookie access_token, refresh_token, __Host-csrf"| C["Caddy"]
C -->|"/api/v1/*"| A["Go API"]Загрузка схемы…
После переезда между Caddy и Go появляется BFF:
Клиент вызывает процедуры tRPC на /api/trpc, данные те же; BFF ходит в Go по GO_API_URL (на одной VPS — http://api:8080/api/v1) с Authorization: Bearer. До infra PR #71 маршрутизация была такой: Caddyfile.j2 (reverse_proxy api:8080); на одной VPS api не публикует порты и виден только из сети app — см. compose.yml.j2.
flowchart LR
B["Браузер"] -->|"cookie __Host-Http-session и заголовок X-CSRF: 1"| C["Caddy"]
C -->|"/api/trpc/* на bff:3000"| P["BFF"]
B -->|"GET HTML, cookie; без X-CSRF"| C
C -->|"HTML на bff:3000"| P
P -->|"HTML с BootstrapData"| B
P -->|"GO_API_URL; Authorization: Bearer"| A["Go API"]Загрузка схемы…
Cookie до и после
| Cookie | Path | HttpOnly | SameSite | Кто читает |
|---|---|---|---|---|
access_token (сейчас) | /api/v1 | да | Lax | Go API |
refresh_token (сейчас) | /api/v1/auth | да | Lax | Go API |
__Host-csrf (сейчас) | / | нет | Lax | фронт читает из document.cookie, Go сверяет |
__Host-Http-session (после) | / | да | Lax | только BFF; в браузере JavaScript её не видит |
Подробности про нынешние cookie — в разделе CSRF. __Host-Http-session — Secure, без Domain, срок Max-Age равен refresh_expires_in.
Почему BFF
RFC 10017 «OAuth 2.0 for Browser-Based Applications» (BCP 212) перечисляет архитектуры браузерных приложений (§6) и ставит BFF первой:
The patterns in this section are presented in decreasing order of security.
Про BFF отдельно (§6.1.4.3):
This architecture is strongly recommended for business applications, sensitive applications, and applications that handle personal data.
У нас личные данные пользователей, так что рекомендация подходит. Практический выигрыш: access и refresh не доходят до браузера, а значит, XSS на странице не может их украсть. Но XSS всё равно может слать запросы через BFF от имени пользователя: это сценарий «Proxying Requests via the User's Browser» в RFC 10017 §6.1.4.1. BFF убирает кражу токенов, но не последствия XSS.
Что меняется
Фронт
Клиент создаётся в src/api/client.ts с baseUrl: '/api/v1' и credentials: 'include'. Его заменяет официальный @trpc/client (см. «Клиент»): типы процедур и HTML — import type { AppRouter, BootstrapData } из BFF, на каждый вызов X-CSRF: 1, cookie он не читает. Refresh на 401 и повтор после 403 уходят; UNAUTHORIZED означает гостя и форму входа. До первого рендера клиент читает JSON из HTML в свой стор сессии, без стартового users.me; процедура остаётся для явного обновления. Некорректный bootstrap — ошибка запуска. Задача frontend#34 становится не нужна.
BFF
Новый сервис на Hono с официальным @trpc/server и роутером tRPC на /api/trpc (см. «Роутер»); каркас, переменные окружения, логи, образ и запуск — «Сервис BFF». createContext сначала проверяет X-CSRF, Origin и Sec-Fetch-Site, затем вызывает общий слой сессии. Его же использует HTML-хендлер; authedProcedure требует сессию, а refresh общий для HTML/RPC. Вызовы Go — fetch-клиент от Orval, вход процедур — zod-схемы от Orval из контракта Go. Работает без своего хранилища сессий. Гонка backend#12 решается внутри BFF, пока он один экземпляр — см. сценарий «Параллельные RPC».
Go API
Authenticate сейчас читает access из cookie access_token, хотя контракт уже говорит про Authorization: Bearer. После переезда он читает Authorization: Bearer, токены отдаются в JSON, а middleware CSRF и CORS, код cookie и переменные CSRF_SECRET, COOKIE_SECURE, CORS_ALLOWED_ORIGINS удаляются. Работа на стороне Go — backend#41.
Infra
В compose добавлен сервис bff с SESSION_KEY и APP_ORIGIN, а Caddy направляет /api/trpc/* на bff:3000, как и HTML. Корневой HTML из S3 заменяется шаблоном из образа BFF; ассеты остаются в CDN. API, public и внутренний /health обрабатываются до HTML fallback. Развёртывание BFF и переключение Caddy подготовлены в infra PR #71 по infra#58; Go и новая БД на VPS2 — infra#59.
Переключение
MVP использует один экземпляр BFF; короткое окно недоступности при выпуске допустимо. Порядок переключения:
- Загрузить ассеты клиента в неизменяемый CDN-каталог своего SHA.
- Подготовить образ BFF с HTML того же SHA после обеих сборок. Упаковка выполняется вне
bff#build; отдельного promote клиента после playbook нет. - После проверки образа и ассетов создать GitHub Deployment для выбранного SHA и вызвать
deploy-bff.ymlс digest BFF. Go выкатывается своим playbook; BFF и Caddy переключаются через entrypoint BFF. Подробности — на странице выкатки. - Проверить фактически запущенный образ, внутренний
/health, гостевой HTML, вход и защищённый RPC, доступность ассетов своего SHA. Лишь после этого поставить Deploymentsuccess. Готовность BFF предполагает проверенные конфигурацию и шаблон; завершение запросов ограничено конечным сроком — подробнее на странице сервиса. - Откат при сбое запускается вручную: он возвращает предыдущий успешный образ вместе с HTML и сохранёнными CDN-ассетами. Совместимость Go и миграций БД проверяется отдельно.
Ответы BFF на auth.login, auth.register и auth.logout стирают три старые cookie с их Path: access_token (/api/v1), refresh_token (/api/v1/auth) и __Host-csrf (/). Каждый пользователь один раз входит заново — см. сценарий «Первый вход после переезда». Обычный релиз с прежним SESSION_KEY сессию не сбрасывает. Публикацию и выпуск автоматизирует frontend#4.
Канарейка — следующий этап
Сначала новая версия получает небольшой стабильный сегмент пользователей; доля растёт после проверки ошибок и задержек. При ухудшении нужен возврат трафика на прежнюю версию. Это отдельное развитие после MVP, со следующими предпосылками:
- HTML и RPC одного пользователя направляются согласованно в выбранную версию.
- Cookie читаются обеими версиями, а refresh безопасен между экземплярами. Текущий in-memory single-flight хранит результат 10 секунд в одном BFF и не создаёт общей блокировки между процессами.
- API совместим с открытыми вкладками: совместный образ HTML/BFF не обновляет уже загруженный JavaScript.
- Изменения Go/БД допускают совместную работу версий и безопасный откат.
Один graceful reload Caddy этих условий не обеспечивает. Сегментация трафика и общий механизм refresh проектируются отдельно; в MVP новые сервисы и распределённые блокировки не добавляются.