> ## Documentation Index
> Fetch the complete documentation index at: https://omniflow-vpn.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Технологический стек

> Каждый компонент, на котором работает Omniflow, почему он выбран вместо очевидной альтернативы и чего в стеке намеренно нет

Omniflow — это модуль Go 1.26 и одно приложение Next.js, разворачиваемые как пять контейнеров плюс обратный
прокси на выбор оператора. Ничто в стеке не требует брокера, оркестратора или среды выполнения плагинов,
потому что целевое развёртывание — один Linux-сервер, которым управляет один человек, он же администратор
VPN-панели.

Каждый выбор ниже описан вместе с альтернативой, которую он вытесняет, чтобы контрибьютор видел, какие решения
несущие, а какие просто текущие.

| Уровень | Технология |
| - | - |
| Бэкенд | Go 1.26, chi v5, pgx v5, sqlc, River v0.43 |
| Данные | PostgreSQL 18, Atlas, Valkey 9 |
| Контракт | OpenAPI 3.0.3, oapi-codegen, Orval |
| Фронтенд | Одно приложение Next.js 16.3, React 19, TypeScript 7, SWR, Zustand, Tailwind v4, shadcn/ui |
| Формы и валидация | React Hook Form, Zod |
| Локализация | next-intl, русский и английский |
| Инструментарий | Bun workspaces, Biome, Playwright, Testcontainers |
| Наблюдаемость | `log/slog`, Prometheus, инструментирование OpenTelemetry |
| Развёртывание | Docker Compose, образы distroless и Alpine, обратный прокси на выбор оператора |

## Go — один модуль с тремя точками входа

`go.mod` объявляет `go 1.26.0` и один модуль, `github.com/omniflow/omniflow`. `cmd/api`, `cmd/bot` и
`cmd/worker` компилируются из одного дерева, поэтому доменное изменение доходит до всех трёх сразу и не бывает
рассинхронизации версий между API, который пишет строку, и воркером, который её читает.

Альтернатива — три сервиса с тремя репозиториями и тремя темпами выпуска — дала бы независимое масштабирование,
которое не нужно ни одной установке такого размера, в обмен на распределённый протокол между оформлением
заказа, правом на услугу и выдачей доступа. Вместо этого значимая граница — транзакция PostgreSQL. См.
[Обзор архитектуры](https://omniflow-vpn.mintlify.site/ru/architecture/overview.md).

Достижимость здесь — настоящее ограничение, а не договорённость: `go list -deps ./cmd/<name>` показывает ровно
то, к чему может обратиться каждый процесс, и бинарник `api` действительно не содержит ни исполнителя
кампаний, ни очистки по срокам хранения.

## chi вместо фреймворка

`github.com/go-chi/chi/v5` — это маршрутизатор поверх `net/http`, а не фреймворк. Каждый обработчик — это
`http.HandlerFunc`, а каждый middleware — обычная `func(http.Handler) http.Handler`, поэтому три префикса могут
нести три по-настоящему разных набора middleware в одном процессе, и ни один из них не наследует чужой.

Два поведения chi являются несущими. `middleware.RequestID` присваивает идентификатор, который попадает в
заголовок ответа `X-Request-ID`, в каждое тело ошибки как `request_id`, в строки аудита, в идентификаторы
корреляции исполнения заказов и в события безопасности клиента — одна строка, связывающая жалобу клиента со
строкой журнала обработчика и вызовом Remnawave. А `chi.RouteContext(...).RoutePattern()` даёт метричному
middleware **шаблон** маршрута для метки, поэтому путь, содержащий идентификатор клиента, никогда не
становится значением метки Prometheus.

## pgx, sqlc и Atlas

`pgx/v5` используется напрямую, без ORM. Запросы лежат как SQL в `database/queries/`, а sqlc генерирует
типизированный Go в `internal/database/dbgen` с `sql_package: pgx/v5`. Это даёт то, что SQL, который читает
ревьюер, — это и есть SQL, который выполняется, включая те части PostgreSQL, которые ORM обычно прячет и на
которые эта кодовая база сильно опирается: `ON CONFLICT … DO UPDATE … RETURNING` для идемпотентных вставок,
`pg_advisory_xact_lock`, чтобы потолок параллелизма выдержал две одновременные покупки, `SELECT … FOR UPDATE`
внутри сериализуемых транзакций, частичные уникальные индексы и ограничения `CHECK`, делающие некорректные
состояния непредставимыми.

Изменения схемы идут в обратном порядке по сравнению с migration-first-инструментами: правите
`database/schema.sql`, генерируете отрецензированную миграцию Atlas командой `make atlas-diff`, затем заново
генерируете sqlc. Миграции — только вперёд: down-миграций нет, а откат схемы — это восстановление из резервной
копии. `database/migrations/atlas.sum` создаётся только командой `atlas migrate hash` и никогда не правится
руками: несовпадение контрольной суммы означает, что миграция изменилась, а не что файл устарел.

CI применяет каждую закоммиченную миграцию от нескольких базовых точек до головы и доказывает, что повторное
применение головы ничего не меняет, а интеграционный набор применяет их в PostgreSQL 18.4 из Testcontainers.
См. [База данных](https://omniflow-vpn.mintlify.site/ru/contributing/database.md).

## River для надёжной работы

`riverqueue/river v0.43.0` с драйвером `riverpgxv5` выполняет задачи в той же базе данных PostgreSQL, что и всё
остальное. В этом и смысл: задачу можно вставить через `InsertTx` внутри той транзакции, которая произвела
требующее её изменение состояния, — чего брокер в отдельной системе не может предложить без двухфазного
протокола или диспетчера outbox. `AGENTS.md` делает это правилом и запрещает вводить брокер без принятого
архитектурного решения.

Настроены три очереди — `critical` (16 воркеров), `default` (8) и `bulk` (4). `fulfillment` и
`goods_delivery` выполняются на `critical` с `MaxAttempts: 12`; `periodic_task`, проходы по всей таблице, —
на `default` с одной попыткой.

> [!NOTE]
>
> Собственные таблицы River поставляются вместе с миграциями Atlas: `20260905130000_river_queue.sql` — это
> дословно скопированная собственная линия миграций River, поэтому сервис `migrate` в compose создаёт
> `river_job` и её собратьев вместе со схемой Omniflow. Интеграционный тест падает, если `rivermigrate` видит
> неприменённую версию River, поэтому обновлению River нужна соответствующая миграция.

Полные проходы по таблице без аргумента для отдельной записи — это периодические задачи River, а не
таймеры: таймер выполняется один раз на процесс, а периодическую задачу выбранный River лидер ставит в
очередь один раз на всю установку, поэтому воркер может работать в нескольких репликах. На `time.Ticker`
остаются только циклы, которые касаются своего процесса. Обе модели перечислены в разделе
[Задачи и очереди](https://omniflow-vpn.mintlify.site/ru/operations/jobs-and-queues.md#периодические-проходы-и-внутрипроцессные-циклы).

## PostgreSQL 18 и Valkey 9

PostgreSQL — единственный надёжный источник истины. Стек compose закрепляет `postgres:18.4-alpine`, а образы
`bot` и `worker` несут `postgresql18-client`, чтобы `pg_dump` и `pg_restore` совпадали с мажорной версией
сервера, — Dockerfile предупреждает, что понижать базу Alpine ниже 3.23 нельзя, поскольку это первый выпуск с
этим пакетом.

Valkey (`valkey/valkey:9.1.1-alpine`) используется для счётчиков ограничения частоты запросов, одноразовых
токенов защиты от повтора, блокировок и кеша и запускается с `--save "" --appendonly no`. Persistence отключён
намеренно: ничто в Valkey не имеет права быть единственной копией чего бы то ни было. Оба примитива — это по
одному Lua-скрипту: `Allow` — это `INCR` плюс `PEXPIRE`, `Once` — это `SET NX PX`, — и оба возвращают ошибку, а
не разрешающее значение по умолчанию, когда клиент отсутствует, поэтому сбой не может молча снять защиту.

Резервное копирование вызывает `pg_dump --format=custom` и `pg_restore`, а не переизобретает их, поэтому
резервная копия всегда восстановима стандартными инструментами PostgreSQL; шифрование — это обёртка AES-256-GCM
с кадрированием поверх этого потока. См. [Резервное копирование и восстановление](https://omniflow-vpn.mintlify.site/ru/operations/backup-restore.md).

## OpenAPI 3.0.3 и сгенерированные привязки

`api/openapi.yaml` — это контракт. Он написан в OpenAPI **3.0.3**, а не в 3.1, потому что у выбранного
генератора Go пока нет стабильной поддержки 3.1, и `AGENTS.md` фиксирует это как условие для перехода.

`make generate` запускает три генератора: `go tool oapi-codegen` для серверных типов Go, `go tool sqlc generate`
для слоя базы данных и `bun run generate:api`, который дважды запускает Orval — один раз для типов TypeScript и
SWR-хуков, другой раз для схем Zod. CI перегенерирует всё это и падает на любом расхождении, поэтому изменение
контракта, закоммиченное без перегенерации, не пройдёт в основную ветку.

Транспорт, который на самом деле использует веб-приложение, написан вручную
(`packages/api-client/src/fetcher.ts`), а не сгенерирован, и это сделано намеренно и задокументировано в самом
файле: SWR-клиент Orval 8.24 выдаёт голые вызовы `fetch` и игнорирует `override.mutator`, поэтому
сгенерированный хук не отправлял бы ни cookie сессии, ни CSRF-токен — каждая мутация через него была бы
отклонена, а каждое чтение было бы анонимным. Обёртка ориентируется на те же сгенерированные типы, так что
формы запроса и ответа остаются сверенными с контрактом; различается только транспорт. Она задаёт
`credentials: "include"`, перечитывает заголовок ответа `X-CSRF-Token` после каждого вызова, чтобы долгоживущая
вкладка пережила ротацию сессии, не трогает `Content-Type` для `FormData`, потому что только браузер знает свою
границу multipart, и превращает ответ-проблему в `ApiError`, чей `code` — последний сегмент `type` проблемы.

Контрибьюторам стоит знать, что сгенерированные серверные типы Go в `internal/httpapi/apigen` сейчас не
импортируются ничем: обработчики декодируют в анонимные структуры и пишут собранные вручную мапы. Ничто не
проверяет ответ по схеме ни на сборке, ни во время выполнения, поэтому изменению схемы нужен тест, который его
проконтролирует.

## Веб-приложение

Одно приложение Next.js 16.3 (`apps/web`) несёт оба интерфейса: `/account` для клиентов и `/admin` для
операторов. Одно приложение вместо двух означает одну систему компонентов, один каталог локализации и одну
сборку, которую нужно держать зелёной, — а разделение маршрутов организационное, поскольку авторизацию
применяет Go API на каждом запросе.

| Задача | Выбор | Почему именно это |
| - | - | - |
| Серверное состояние | SWR 2.5 | Ревалидация и ключирование кеша против REST API, без клиентского хранилища, притворяющегося базой данных |
| Сложное локальное состояние | Zustand 5 | Зарезервирован для состояния, которое действительно охватывает несколько компонентов; обычные экраны используют состояние React |
| Формы | React Hook Form 7.85 | Неконтролируемые поля, поэтому длинная операторская форма не перерисовывается на каждое нажатие клавиши |
| Валидация | Zod 4.4 | Применяется на границах доверия и генерируется из того же файла OpenAPI, что и типы |
| Оформление | Tailwind v4 с shadcn/ui в `packages/ui` | Обе панели используют одни примитивы, поэтому экран оператора и экран клиента не могут визуально разойтись |
| Локализация | next-intl 4.13 | Все пользовательские тексты живут в русском и английском каталогах сообщений; ни одна строка не вшита в компонент |

Продакшен-образ собирается с Bun 1.3.14 и отдаёт standalone-сборку Next.js из `node:26.7-alpine` на порту 3000.

## Инструментарий

Bun workspaces (`apps/*`, `packages/*`) отвечают за установку и скрипты, `bun.lock` закоммичен, и рядом нет
lock-файлов npm, pnpm или Yarn — один lock-файл означает один граф зависимостей. Biome 2.5 — единственный
форматтер и линтер для TypeScript и JavaScript, поэтому форматирование никогда не становится темой ревью.
TypeScript — версии 7.0, и сгенерированные клиенты не должны зависеть от удалённых программных API TypeScript.

Тестирование делится на три части по тому, что может доказать каждый слой. `go test -race -cover ./...`
работает вообще без демона Docker, потому что наборы на Testcontainers лежат в `internal/integrationtest` за
тегом сборки `integration`. Эти наборы применяют каждую закоммиченную миграцию в настоящий PostgreSQL и
прогоняют репозитории. Playwright работает против **продакшен**-сборки, исходя из того, что проверка, проходящая
против сборки, которую никто не выкладывает, — это не проверка.

## Наблюдаемость

`log/slog` пишет JSON в stdout у API и бота и в stderr у воркера, на уровне по умолчанию для обработчика.
Переменной окружения для уровня, формата или назначения журнала нет нигде в репозитории, поэтому повышение
подробности — это изменение кода. Общий редактор вырезает токены ботов, значения bearer и учётных данных,
последовательности цифр, похожие на карту, пары `hwid` и `charge_id`, а также любой URL со схемой `http(s)`,
`vless`, `vmess`, `trojan` или `ss` — целиком, потому что даже хост идентифицирует установку, к которой
подключается клиент. Заявленный компромисс таков: перередактированная строка журнала — небольшое неудобство, а
утёкшая ссылка подписки — инцидент безопасности.

Метрики Prometheus берутся из реестра, отдельного для каждого процесса (не из глобального), и отдаются на
`/metrics` с постоянной меткой `service` со значением `api`, `bot` или `worker`. Каждая метка — ограниченное и
неидентифицирующее значение, потому что значения меток хранятся у цели сбора бессрочно.

Инструментирование OpenTelemetry подключено в четырёх точках: `otelhttp` на входящем маршрутизаторе и исходящем
клиенте, `otelpgx` на пуле соединений, спан на каждое обновление Telegram и спан на каждую задачу исполнения
заказа. `otelpgx` записывает текст запроса и никогда — значения аргументов; исходящий клиент записывает только
метод и хост.

> [!NOTE]
>
> Omniflow никогда не устанавливает провайдер трассировки. `otel.SetTracerProvider` не встречается нигде, ни
> один экспортёр OTLP не импортируется, и ни один процесс не читает переменные `OTEL_*`. Поэтому глобальный
> провайдер является заглушкой, и описанные выше спаны никуда не экспортируются. Инструментирование на месте,
> экспортёра нет.

[Наблюдаемость](https://omniflow-vpn.mintlify.site/ru/operations/observability.md) перечисляет пробы, метрики, которые действительно пишутся, и
строки журнала, по которым стоит настраивать оповещения.

## Развёртывание

`compose.yaml` в корне репозитория описывает весь односерверный стек: `postgres`, `valkey`, одноразовый сервис
`migrate` на `arigaio/atlas:1.3.0`, затем `api`, `bot`, `worker` и необязательный `web` под профилем `web`.
Хостовые порты публикуют только `api` и `web`; PostgreSQL и Valkey остаются во внутренней сети.

Образы собираются из `build/go.Dockerfile` с `CGO_ENABLED=0 go build -trimpath`. `RUNTIME=static` даёт
distroless-образ `nonroot` для API; `RUNTIME=postgres` даёт образ Alpine с клиентом PostgreSQL для бота и
воркера, которым он нужен для резервных копий. Выпущенные образы публикуются в GHCR для `linux/amd64` и
`linux/arm64` с provenance и SBOM.

Обратный прокси не входит в поставку и не обязателен. В `deploy/proxies/` лежат проверенные в CI примеры для
Caddy и Traefik, которые терминируют TLS, ограничивают размер тела запроса, выставляют заголовки безопасности
и — что важнее всего — убирают `/metrics` и `/readyz` с публичного хоста API. Пример для Traefik отключает
журналы доступа, потому что путь запроса может быть ссылкой подписки. См.
[Обратный прокси](https://omniflow-vpn.mintlify.site/ru/operations/reverse-proxy.md).

## Чего намеренно нет

* **Брокера сообщений.** Доменные события идут через транзакционный outbox, и `AGENTS.md` требует принятого
  архитектурного решения до того, как брокер будет введён.
* **Фреймворка внедрения зависимостей.** Связывание явно прописано в каждом `main` — многословно и читаемо;
  оно изменится, когда сложность связывания этого действительно потребует.
* **ORM.** SQL пишется, рецензируется и генерируется в типы.
* **Kubernetes, Helm и Pulumi.** Единственная топология развёртывания в репозитории — Docker Compose плюс два
  примера прокси.
* **Среды выполнения плагинов.** Нет ни WASM-хоста, ни ABI плагинов, ни точки расширения, загружающей
  сторонний код в процесс.
* **Второго места, которое решает, что позволено роли.** Каталог прав доступа и сопоставление ролей
  вкомпилированы в `internal/rbac`, поэтому правка в базе данных не может расширить доступ.

## Смежные материалы

> **[Обзор архитектуры](https://omniflow-vpn.mintlify.site/ru/architecture/overview.md)**
>
> Процессы, пути запросов и транзакционный шаблон.

> **[Разработка](https://omniflow-vpn.mintlify.site/ru/contributing/development.md)**
>
> Настройка рабочего пространства, генераторы, запуск стека и проверки.

> **[База данных](https://omniflow-vpn.mintlify.site/ru/contributing/database.md)**
>
> Схема, Atlas, sqlc и правила миграций.

> **[Развёртывание](https://omniflow-vpn.mintlify.site/ru/operations/deployment.md)**
>
> Топология compose, тома, обновления и откат.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.