Files
ksv741 329d66e767
Specs security / security (push) Successful in 8s
Specs verify / verify (push) Successful in 11s
spec: finalize wealth-ledger SDD migration
2026-10-09 21:14:22 +03:00

21 KiB
Raw Permalink Blame History

Context

Frontend и backend выпускаются из независимых Git-репозиториев. Frontend — Vite SPA, backend — NestJS API с embedded SQLite, Prisma migrations, native Node dependencies и runtime .proto contracts. Браузерная сессия использует Bearer access token и Secure httpOnly refresh-cookie с path /api/v1/auth.

На production-сервере уже работает Caddy. Он должен знать только домен и один loopback port WealthLedger; TLS и server-wide routing остаются вне приложения. Краткий простой обновляемого компонента допустим. Текущие кэши, rate limiter и connection generation допускают только один активный backend instance.

Root workspace связывает независимые checkout, но его CI по действующему контракту выполняет только commitlint. Поэтому orchestration принадлежит root, а сборка и публикация каждого image — репозиторию компонента.

Goals / Non-Goals

Goals:

  • запускать весь application runtime локально через Docker с Linux, общей container network, единым browser origin и быстрым frontend/backend watch;
  • поставлять отдельные immutable frontend и backend images;
  • публиковать наружу только frontend port, через который доступны SPA и API;
  • хранить SQLite и backend secrets вне images и сохранять их при recreation;
  • применять Prisma migrations до готовности новой версии backend;
  • обновлять frontend или backend отдельно с явной парой установленных версий;
  • дать оператору воспроизводимые backup, rollback и restore boundaries.

Non-Goals:

  • zero-downtime, blue-green, несколько backend replicas и высокая доступность;
  • PostgreSQL, Kubernetes, container orchestration вне одного сервера;
  • управление Caddy, DNS, TLS, host firewall или provisioning сервера;
  • изменение HTTP API, session semantics или пользовательского интерфейса;
  • общий release номер для независимых репозиториев.

Decisions

Уточнение Change Owner от 2026-09-16: обязательная настройка digests отменена. Production принимает FRONTEND_IMAGE/BACKEND_IMAGE с release version tags. Публикация сохраняет запрет перезаписи release tags; latest не используется.

1. Docker Compose владеет application topology

Root workspace получает production Compose и development override. Production Compose использует опубликованные image references; development override заменяет их local development targets, подключает исходники и отдельное local data storage. Новые custom orchestration scripts не создаются: lifecycle выражается штатными Compose commands и документированной последовательностью.

Production содержит два long-running services:

  • frontend: nginx со статической SPA и proxy /api/ в backend:3000;
  • backend: один NestJS process с доступом к persistent SQLite storage.

Одноразовый migration service использует тот же backend image и data storage, завершается до старта новой версии backend. Наружу публикуется только nginx на настраиваемый loopback host port. Caddy проксирует весь домен на этот port и не маршрутизирует /api/ отдельно.

Development сохраняет те же component и network boundaries, но frontend target запускает Vite с proxy /api/ в backend:3000, а backend target запускает Nest watch. Browser видит только frontend port. Source mounts не включают host node_modules; Linux dependencies принадлежат images/volumes. Изменение manifest или lockfile требует rebuild соответствующего development image.

Отдельный nginx gateway в development не добавляется: он создал бы третий постоянный service, но всё равно использовал бы отличающуюся proxy-конфигурацию для Vite HMR. Production image и его nginx routing проверяются отдельным production Compose smoke scenario.

2. Frontend и backend имеют собственные multi-stage images

Frontend image собирает SPA точной project Node version и переносит только build output и nginx configuration в version-pinned nginx runtime. Production SPA использует same-origin API base. nginx поддерживает SPA history fallback, передаёт /api/ без переписывания prefix и повторно разрешает Docker DNS имя backend после recreation.

Backend image собирается под Linux точной project Node version. Runtime содержит compiled application, production dependencies, generated Prisma client, migrations и T-Bank .proto contracts по пути, который использует production process. Prisma CLI становится явным runtime tool backend image; test, lint и compiler tooling в runtime не переносится. Process работает не от root и пишет только в выделенный data path.

Оба images получают OCI metadata с repository revision и version. Base images и package installs закрепляются воспроизводимыми версиями; новая platform или architecture не обещается без отдельной проверки.

3. Gitea Container Registry хранит независимые release images

Frontend и backend workflows публикуют собственный image после push защищённого release tag vX.Y.Z. Publish job повторяет канонический repository gate, проверяет совпадение tag с package version и только затем отправляет semver tags в Gitea Container Registry. latest не используется.

Server deployment фиксирует полные frontend/backend image references с semver tags; оператор выбирает опубликованную версию каждого компонента. CI использует отдельный secret с package write permission, сервер — отдельный credential только для pull. Ни один credential не хранится в repository или image. Возможность встроенного Gitea workflow token публиковать packages не предполагается без проверки фактической server version.

Root не получает image workflow: каждый repository самостоятельно доказывает и публикует свой artifact, а Compose только фиксирует установленную пару image references.

4. Same-origin routing сохраняет действующую auth boundary

Production frontend nginx и development Vite направляют исходный /api/... в backend без изменения path. Frontend client использует relative API base. Backend получает FRONTEND_ORIGIN, равный browser-visible origin; production работает с NODE_ENV=production, поэтому refresh-cookie остаётся Secure.

nginx передаёт host, forwarding headers и request ID context, не журналирует Authorization, Cookie или request body. Backend port доступен только Compose network. Server host публикует frontend на loopback, чтобы Caddy оставался единственным внешним listener.

5. Readiness опирается на существующий runtime contract

Migration service должен завершиться успешно до запуска backend. Backend healthcheck подтверждает завершённый HTTP-ответ от существующего auth route; статус 401 без cookie является допустимым доказательством готового HTTP/auth boundary. Новый публичный health endpoint не добавляется. Frontend healthcheck получает статический root и зависит от healthy backend при полном старте.

Compose --wait используется как bounded readiness gate. Он не является автоматическим rollback и не обещает отсутствие краткого 502 во время отдельного backend update или недоступность всей точки входа во время frontend recreation.

6. SQLite остаётся single-server storage в явно ограниченной роли

Production использует один backend process и один persistent host data directory. Это расширяет прежнее локальное решение ADR 001: docs получают новый ADR, который разрешает SQLite только для текущего single-server deployment и фиксирует условия пересмотра — несколько replicas, иной availability target, рост write contention или потребность в отдельном database service.

Перед migration backend останавливается и создаётся согласованная копия всего data directory. Остановка исключает конкурентные writes и позволяет сохранить database вместе с возможными WAL companion files без нового backup service. Encryption key хранится отдельно от data backup; восстановление требует оба артефакта.

Local development использует другой named volume/data directory и другие secrets. Production data никогда не монтируется в development environment.

7. Обновления меняют только выбранный service

Frontend update меняет только frontend image reference, загружает image и recreates frontend без зависимостей. Backend и data остаются запущенными, но единая внешняя точка входа кратко недоступна во время recreation.

Backend update сначала сохраняет backup при остановленном backend, затем меняет backend image reference. Migration service и backend используют один reference; backend стартует только после успешной migration и становится доступен после healthcheck. Frontend не пересоздаётся. nginx повторно разрешает service DNS после смены backend container address.

Release compatibility остаётся обязанностью API evolution: новая сторона ставится первой только когда прежняя сторона совместима с ней. Compose-файл фиксирует пару image references, но не вводит второй version negotiation mechanism.

UI/UX Applicability and Package

Applicability and scope: N/A — change меняет development, packaging и server operations, но не экраны, состояния, interactions или responsive поведение WealthLedger. Новый визуальный пакет не требуется.

Delivery Map

Area Status Architectural outcome or N/A reason
frontend affected Multi-stage development/production image, same-origin client configuration, Vite API proxy, nginx SPA/API routing и image publication workflow.
backend affected Multi-stage development/production image, runtime Prisma migration assets, container readiness и image publication workflow.
specs affected Новая capability application-delivery и утверждаемый SDD package.
docs affected Deployment/runbook, обновлённая container topology и ADR ограниченного single-server SQLite deployment.

Root workspace дополнительно владеет Compose orchestration и кратким entrypoint README как cross-area coordination; это не переносит component build ownership из frontend/backend и не добавляет root CI помимо commitlint.

Solution Assessment

Вариант Fit и зрелость Security / operations Ограничения и migration cost Решение
Docker Compose + два component images Нативно покрывает один server, service DNS, healthchecks, volumes и targeted recreation; широко поддерживается Малый operating surface; registry credentials и host Docker требуют обычной защиты Краткий простой, один host, нет automatic rollback Выбран как минимальное полное решение
Один объединённый application image Простая установка одного artifact Один lifecycle и один process boundary проще оператору Связывает независимые релизы, пересобирает неизменённый component, усложняет watch и нарушает repository ownership Отклонён
Kubernetes или Swarm Зрелая orchestration для replicas и rolling updates Добавляет control plane, политики и постоянную эксплуатацию Не нужен одному server и не решает текущие SQLite/multi-instance ограничения Отклонён до появления HA требований
Caddy маршрутизирует отдельно frontend и backend Использует существующий server proxy Убирает internal gateway layer Server должен знать внутреннюю topology, появляются два application upstream и deployment перестаёт быть self-contained Отклонён по выбранной внешней границе
Отдельный постоянный gateway service внутри Compose Стабильный port при frontend recreation Может уменьшить внешний простой frontend update Третий runtime service и конфигурация без требования zero-downtime Отклонён; пересмотреть при SLO без краткого простоя
SQLite на persistent directory Уже реализован, зрел для single-process embedded workload, не требует database service Минимальная сеть и credentials; backup требует остановки Один writer/process topology, нет HA; новая ADR boundary обязательна Выбран в ограниченной single-server роли
PostgreSQL service Зрел для concurrency, replicas и независимого DB lifecycle Требует отдельные credentials, backup, upgrades и monitoring Миграция Prisma provider и существующих данных вне scope; текущая нагрузка не требует Отклонён до появления multi-instance/availability требования
Собственная packaging/orchestration layer Может точно повторить частные команды Создаёт собственный security и maintenance surface Дублирует Compose, registry и standard image lifecycle Отклонён

Gitea Container Registry и Actions используются как уже доступные mature platform capabilities. Предварительное условие Apply — runner с Docker build/push capability и package permissions; при отсутствии этого условия Design не заменяется самописным registry uploader, а publish task остаётся blocked.

Risks / Trade-offs

  • Frontend recreation кратко отключает и SPA, и API ingress; это принято текущим non-goal zero-downtime.
  • Backend update может кратко возвращать 502; healthcheck ограничивает окно, но не устраняет его.
  • Старые browser tabs могут запрашивать chunks прежнего frontend. nginx задаёт revalidation для HTML и immutable cache для hashed assets, но прежние assets после recreation не сохраняются. Пользователю может потребоваться reload; автоматическое восстановление старых вкладок остаётся вне текущего scope.
  • SQLite на server расширяет прежний local-only baseline. Новый ADR делает это осознанным ограничением, а не молчаливым изменением production guarantee.
  • Воспроизводимость version tags зависит от запрета их перезаписи: workflow отклоняет существующий tag. Переназначать опубликованные tags нельзя.
  • Development работает через Vite, production — через nginx. Production Compose smoke проверяет различающийся web-server boundary до поставки.
  • Полная runtime image воспроизводимость зависит от base-image availability; digests и сохранённые release images уменьшают, но не устраняют registry risk.

Migration Plan

  1. Утвердить SDD package и новый ADR о границе SQLite на одном сервере.
  2. Подготовить component images и проверить их отдельно с каноническими gates.
  3. Добавить root Compose topology и development override; подтвердить watch, same-origin API, отсутствие внешнего backend port и изоляцию local data.
  4. Добавить release-tag publication в component repositories и получить version tags проверенных images из Gitea Registry.
  5. На server создать restricted environment/secrets и data directories, сохранить исходную базу и encryption key, затем выполнить первый migration service и production smoke через loopback port.
  6. Настроить Caddy на единственный loopback port и проверить login, refresh, чтение существующих данных и один внешний financial request.
  7. Зафиксировать установленную пару image references и удалить прежний runtime только после успешной проверки нового.

Rollback Contract

Field Decision
Status required
Reason Change вводит server persistence, migrations, secrets и новую delivery boundary.
Trigger Migration failure, unhealthy component, невозможность login/refresh, нечитаемое encrypted connection, повреждение данных или критическая регрессия smoke scenario.
Actions Не направлять Caddy на первый deployment либо вернуть предыдущий component image reference. Старую версию backend запускать только после проверки совместимости с текущей schema. Если schema несовместима, остановить приложение и отдельно предложить восстановление согласованного backup; не выполнять restore автоматически.
Data safety Backup создаётся при остановленном backend из всего data directory; encryption key сохраняется отдельно. Данные, созданные после backup, считаются более новыми и не заменяются без явного решения оператора о потере этого интервала.
Verification Проверить component health, schema migration status, login/refresh, чтение существующих user/card data, decrypt/use сохранённого T-Bank connection и отсутствие повторной migration ошибки; затем записать фактически установленную пару image references.