# Приложения, хранилище и взаимодействия
Представление C4 Container раскрывает внутреннее устройство WealthLedger.
Контейнер здесь означает приложение или хранилище, а не Docker-контейнер.
Внешние системы совпадают с [System Context](context.md).
## Представление Container
```mermaid
flowchart LR
U["Частный инвестор
Человек"]
subgraph W["WealthLedger · программная система"]
F["React SPA
TypeScript, Vite, Mantine, TanStack Query"]
B["Backend API
NestJS, Node.js"]
D[("Локальная база
Embedded SQLite")]
F -->|"HTTP JSON /api/v1; Bearer и auth cookie"| B
B -->|"Prisma + better-sqlite3; доступ к локальному файлу"| D
end
T["T-Bank Invest API
Внешняя система"]
M["MOEX ISS
Внешняя система"]
N["Реестр НКР
Внешняя система"]
C["T-Bank Brands CDN
Внешняя система"]
U -->|"Просматривает данные и управляет подключением"| F
B -->|"gRPC over TLS; token пользователя"| T
B -->|"HTTPS; публичные данные"| M
B -->|"HTTPS; публичный HTML"| N
F -->|"HTTPS; изображения без авторизации"| C
```
SPA выполняется в браузере пользователя. Backend — отдельное
приложение; SQLite используется внутри его процесса через библиотеку и файл
базы. Между API и SQLite нет сетевого database server. Граница системы на
схеме показывает принадлежность частей, а не их размещение на одном узле.
## Контейнерная поставка
Deployment view для одного сервера. Caddy принадлежит серверу; Compose
содержит два постоянных application containers и одноразовую migration.
```mermaid
flowchart LR
Browser["Браузер · React SPA"] -->|HTTPS| Caddy["Серверный Caddy · TLS"]
subgraph Host["Один Docker host"]
Caddy -->|"HTTP · 127.0.0.1:8080"| Frontend["frontend · nginx · SPA + /api/"]
Frontend -->|"HTTP /api/v1 · internal network"| Backend["backend · NestJS · один process"]
Migration["migration · Prisma · one-shot"] -->|"До старта backend"| Data[("Host directory · SQLite + WAL")]
Backend -->|"Чтение и запись /data"| Data
Secrets["Restricted environment · ключи вне images"] --> Backend
end
```
Только frontend публикует host port, привязанный к loopback. Backend не
публикует порт, но имеет исходящий доступ к внешним API. nginx сохраняет
`/api/` prefix, forwarding headers и request ID; Docker DNS разрешается
повторно после backend recreation. HTML требует revalidation, hashed assets
имеют immutable cache. Caddy не содержит отдельного маршрута к API.
Development заменяет nginx на Vite с HMR, backend — на Nest watch, а host data
path — на отдельный named volume. Browser origin остаётся единым. Linux
node_modules не монтируются с хоста. Production SPA собирается с relative API
base; production cookies остаются Secure, публичный origin использует HTTPS.
Компоненты имеют независимые images и версии. Production фиксирует image
version tags; migration использует тот же backend image. Успех migration предшествует
backend healthcheck на существующем auth route; frontend ждёт healthy backend.
Runtime работает non-root, SQLite сохраняется вне image, ключ шифрования и JWT
secret поступают из restricted environment и сохраняются между обновлениями.
Ограничение SQLite расширено в [ADR 015](../adr/015-single-server-sqlite-deployment.md).
[Runbook](deployment.md) описывает установку, independent
updates, backup, rollback и explicit restore. Он также перечисляет внешние
prerequisites: локальный smoke не подтверждает registry/server rollout.
## Ответственность частей
| Часть | Ответственность |
| --- | --- |
| React SPA | Экраны, навигация, формы, состояние входа и клиентский кэш ответов; форматирование денежных значений |
| Backend API | Локальная идентичность, сессии, проверка доступа, подключение брокера, нормализация внешних данных, точные расчёты и синхронизация сохранённых источников |
| SQLite | Долговременное хранение локальных пользователей, сессий, зашифрованных подключений, ручной банковской истории и инвестиционных источников с coverage/work |
| Внешние системы | Данные брокера, публичные сведения об инструментах и изображения в пределах своей ответственности |
Frontend организован слоями `app → pages → features → entities → shared`:
каркас связывает маршруты, страницы собирают сценарии, а общие средства
обслуживают транспорт и представление. Backend остаётся модульным NestJS API,
где локальная аутентификация отделена от подключения брокера и инвестиционных
сценариев. Это внутренняя организация приложений, не набор микросервисов.
Маршруты и AppShell принадлежат `app`. Page controller/VM управляет запросами,
формой и восстановлением; presentation получает данные и callbacks, поля
могут использовать контекст React Hook Form. В OperationForm controller
сохраняет intent операции, черновик и явное принятие актуальной версии после
конфликта. В account-income чистые преобразования периода принадлежат модели
страницы и не связывают форму с её запросами.
Локальные packages tokens/ui/icons образуют [визуальную основу](../design/overview.md#общая-основа-и-каталог)
внутри frontend и входят в сборку SPA. Это библиотеки, а не дополнительные
runtime-контейнеры. Провайдеры темы принадлежат приложению и Storybook;
предметные компоненты и API-клиент остаются в приложении. Отдельный API-пакет
и registry-поставка визуальных пакетов не реализованы. Существующие Docker
images приложения не означают независимого npm-выпуска этих библиотек.
Финансовые HTTP-запросы браузера идут через backend. Пользователь вводит token
T-Bank в форме и отправляет его для сохранения, но API не возвращает token
в ответах; SPA не использует его для прямых брокерских запросов. Прямая
загрузка публичного изображения из CDN не проходит через эту финансовую
границу.
Типизированный frontend-клиент производится из предоставленного OpenAPI snapshot
во время разработки. В runtime SPA вызывает API, а не генератор или OpenAPI
endpoint. Форма запросов и ошибок описана в [HTTP API](http-api.md); направление
генерации принято в [ADR 002](../adr/002-nest-generated-openapi-contract.md),
а доверие frontend к предоставленному snapshot — в
[ADR 014](../adr/014-trusted-openapi-input.md).
## Хранимые данные и границы доступа
SQLite содержит пользователя с нормализованным email и hash пароля, его
сессии с hash текущего refresh token, сроком действия и признаком отзыва,
а также одно зашифрованное подключение T-Bank с временем подключения.
Пароли хешируются bcrypt; token брокера шифруется AES-256-GCM. Plaintext
credentials не являются сохраняемым представлением этих данных.
Ручной банковский учёт хранит карты, пользовательские категории и операции
в отдельных таблицах, связанных с User. Начальный остаток и суммы записаны
целыми bigint-копейками, календарные даты — днями без времени. Перевод хранится
одной операцией с двумя картами; возврат связан с исходной покупкой. Баланс
вычисляется из неудалённой истории и отдельно не сохраняется.
Банковские запросы проверяют локального владельца каждой связи и не используют
подключение брокера. Транзакции, ограничения уникальности и версии согласуют
переводы, возвраты и конкурирующие исправления. Fingerprint исходного ввода
и отметка удаления защищают от повторного создания после потерянного ответа
или перезапуска. Архив обратим и сохраняет историю. Подробности —
[банковские карты](../domains/cards/overview.md).
Сессии и подключение связаны с владельцем. Backend локально проверяет
membership текущей revision перед каждым финансовым чтением. Unknown membership
не открывает доступ к произвольному счёту; известный отказ токена блокирует
личные данные. Ротация refresh token остаётся атомарной.
SQLite хранит полный последний portfolio, операции и их подтверждённые
диапазоны, private прогнозы, общие instrument/market sources и intent/staging.
Нормализованные финансовые значения сохраняются decimal-строками. Полнота
отделена от свежести и пагинации. Ответ не ждёт внешних RPC; запрос при
необходимости запускает работу в том же backend process через существующий
лимитер. На startup импорт не запускается; восстановление требует следующего
запроса. Подробности — [модель свежести](cross-cutting/data-freshness.md).
PUT подключения сначала проверяет token у T-Bank, затем одной транзакцией
сохраняет encrypted token, новую revision и проверенный membership.
Сетевая операция не удерживает SQLite-транзакцию. Общие источники не содержат
личных позиций, ожидаемых сумм или credentials. Произвольного архива
отключённых счетов нет.
## Сохранение и восстановление данных
Поддерживаемая процедура останавливает backend и копирует весь data directory
с возможными WAL/SHM files до migration. Копирование отдельных файлов
работающей базы не сохраняет целостный снимок; для live backup потребовался бы
SQLite backup API. Восстановление проверяется на отдельной копии. Ключ шифрования подключения хранится отдельно от базы;
его потеря делает сохранённый T-Bank token нечитаемым.
Откат приложения требует совместимости предыдущей версии с сохранённой
схемой. Расширенная банковскими таблицами схема сохраняет пользователей,
сессии и подключения. Возврат старого backup поверх новой истории удалил бы
введённые банковские операции; откат приложения не равнозначен откату данных.
Совместимость произвольных будущих версий этим не гарантируется.
## Стратегия решения и её основания
Локальная база и Prisma дают типизированный доступ к локальному состоянию
без отдельного сервера базы; исходные причины и пределы выбора сохранены
в [ADR 001](../adr/001-prisma-sqlite-local-mvp.md). Централизованная
[серверная граница T-Bank](../adr/003-tbank-server-side-grpc-boundary.md)
сосредоточивает credentials, лимиты и повторы вне браузера, а
[шифрование подключения](../adr/004-encrypted-per-user-tbank-token.md)
задаёт способ хранения брокерского секрета.
[Политика сессий](../adr/006-auth-session-policy.md) разделяет короткоживущий
access token и вращаемый refresh token. [Политика query-кэша](../adr/007-react-query-fetch-policy.md)
сохраняет доступные данные при обновлении и не запускает refetch при возврате
фокуса. [Ограниченные повторы T-Bank](../adr/005-tbank-upstream-resilience.md)
и [структурированные журналы](../adr/008-pino-backend-logging.md) обслуживают
внешнюю границу. ADR сохраняют контекст принятия решения; текущие детали
уточняются в тематических описаниях.
## Качество и ограничения
Точность представления денег требует сохранять валюту и decimal-значение;
неизвестная сумма не заменяется нулём. Свежесть наблюдения не равна
актуальности рынка в реальном времени. Временные ошибки внешнего поставщика
обрабатываются ограниченными повторами и локальными состояниями интерфейса,
без обещания успешного ответа при недоступном upstream.
Авторизация доступа, недопущение token в публичный ответ и атомарность
ротации сессии — отдельные свойства; наличие типизированного клиента
не доказывает их. Известные расхождения регистрации и деталей валидации
отмечены в [аутентификации](../domains/authentication/overview.md#регистрация-и-вход)
и [HTTP API](http-api.md#error-contract).
Поддерживается локальный Compose и ограниченная single-server topology с SQLite.
Origin браузера, адрес API и cookie policy должны быть согласованы.
`localhost` и `127.0.0.1` задают разные browser-границы для cookie и CORS.
Локальный HTTP поддерживается в development-режиме backend; в остальных
режимах refresh-cookie имеет Secure flag и требует защищённого соединения.
Single-server deployment не задаёт гарантий HA или zero-downtime.
Coordinator и лимитер T-Bank действуют на один экземпляр backend. Сохранённые
данные переживают перезапуск; processRunId не является distributed lease. Ротация ключа шифрования
подключений и согласование изменений подключения между распределёнными
экземплярами не входят в реализованный механизм. Эти ограничения не
позволяют переносить локальную схему на несколько экземпляров без отдельного
решения о состоянии, credentials и лимитах.
Runtime-сценарии раскрываются у соответствующих владельцев:
[восстановление сессии](../domains/authentication/overview.md#восстановление-сессии),
[сохранение подключения](../integrations/tbank.md#проверка-и-сохранение-подключения)
и [загрузка портфеля](../domains/portfolio/flows/portfolio-loading.md).