> ## 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 с тремя точками входа (`cmd/api`, `cmd/bot`, `cmd/worker`), собираемыми из
одного дерева исходников в три образа, плюс одно приложение Next.js, несущее оба браузерных интерфейса. Они
используют одну базу данных PostgreSQL и один экземпляр Valkey. PostgreSQL — единственный надёжный источник
истины; Valkey хранит кеш, счётчики ограничения частоты запросов, блокировки и временное состояние, а стек
compose запускает его с `--save "" --appendonly no`, чтобы ни у кого не возникло соблазна считать его
хранилищем.

Разделение на три процесса сделано ради изоляции отказов и различий во времени жизни, а не ради
масштабирования. Пакет из четырёхсот изменений подписок не должен быть привязан ко времени жизни HTTP-запроса,
который его начал, а автоматические продления — единственное место, где деньги движутся без наблюдателя, —
выполняются там, где ни один запрос не может их отменить.

## Четыре процесса

| Процесс | Точка входа | Слушает | Отвечает за |
| - | - | - | - |
| `api` | `cmd/api` | `APP_HTTP_ADDR`, по умолчанию `:8080` | HTTP API (`/v1/catalog`, `/v1/payments/webhooks/{provider}`, `/v1/admin`, `/v1/panel`, `/v1/account`), пробы и `/metrics`, автоопределение режима обслуживания, анонимный телеметрический сигнал и токен первичной настройки администратора |
| `bot` | `cmd/bot` | По умолчанию long polling; `APP_BOT_HTTP_ADDR`, по умолчанию `:8082`, в режиме вебхука; пробы и `/metrics` — только если задан `APP_BOT_METRICS_ADDR` | Обновления Telegram, отправку уведомлений операторам, повторную проверку обязательной подписки на канал, доставку уведомлений клиентам и интерфейс резервного копирования и восстановления `/ops` |
| `worker` | `cmd/worker` | `APP_WORKER_HTTP_ADDR`, по умолчанию `:8081` | Воркеры задач River (исполнение заказов, доставка цифровых товаров) и все периодические проходы: обслуживание заказов, кошельков и корзин, сверка платежей, планировщики сверки и доставки, продления и напоминания о неоплате, обходчик жизненного цикла, раскрытие кампаний, исполнитель массовых операций, проходы очистки по срокам хранения и запланированные зашифрованные резервные копии |
| `web` | `apps/web` | `3000` | Одно приложение Next.js, отдающее клиентскую панель на `/account` и рабочее место оператора на `/admin` поверх одной общей системы компонентов shadcn |

Три образа Go различаются только тем, что им нужно во время выполнения: `api` поставляется на distroless-базе,
а `bot` и `worker` — на Alpine с `postgresql18-client`, потому что оба вызывают `pg_dump` и `pg_restore`.
Веб-образ отдаёт standalone-сборку Next.js на базе Node. См.
[Развёртывание](https://omniflow-vpn.mintlify.site/ru/operations/deployment.md).

### Без чего каждый процесс отказывается стартовать

Конфигурация читается из переменных окружения один раз, при старте, и неверное значение приводит к фатальной
ошибке, а не остаётся скрытой проблемой. Ничто не перечитывает окружение, поэтому любое изменение — это
перезапуск.

* **`api`** запускается с пустым `APP_DATABASE_URL` и тогда обслуживает только `/healthz`, `/livez`, `/readyz`
  и `/metrics`. Если URL базы данных задан, дополнительно требуются `APP_OPERATOR_TOKEN`, 32-байтный
  `APP_DATA_ENCRYPTION_KEY`, `APP_REMNAWAVE_URL`, `APP_REMNAWAVE_TOKEN` и `APP_VALKEY_URL`, иначе процесс
  завершается с сообщением
  `commerce requires APP_OPERATOR_TOKEN, APP_DATA_ENCRYPTION_KEY, APP_REMNAWAVE_URL, APP_REMNAWAVE_TOKEN, and APP_VALKEY_URL`.
* **`bot`** с пустым `APP_TELEGRAM_TOKEN` пишет в журнал `telegram bot disabled because APP_TELEGRAM_TOKEN is empty`
  и простаивает, пока не получит сигнал, — это поддерживаемая конфигурация, а не ошибка. С токеном он
  требует `APP_DATABASE_URL`, `APP_VALKEY_URL`, `APP_REMNAWAVE_URL` и `APP_REMNAWAVE_TOKEN`. Valkey здесь
  обязателен, потому что ограничение частоты запросов и защита от повтора callback-ов прикрывают нажатия,
  которые двигают деньги.
* **`worker`** требует `APP_DATABASE_URL`, `APP_REMNAWAVE_URL` и `APP_REMNAWAVE_TOKEN`.
  `APP_DATA_ENCRYPTION_KEY` необязателен: без него воркер пишет `digital goods delivery disabled` и
  выполняет всё остальное.

Полный список — в разделе [Конфигурация](https://omniflow-vpn.mintlify.site/ru/getting-started/configuration.md).

## Как проходит запрос

```mermaid theme={null}
flowchart LR
  C[Браузер клиента] -->|"/v1/account"| API[api]
  T[Telegram] --> BOT[bot]
  O[Браузер оператора] -->|"/v1/panel"| API
  S[Скрипт или интеграция] -->|"/v1/admin"| API
  P[Платёжный провайдер] -->|"вебхук"| API
  API --> PG[(PostgreSQL)]
  BOT --> PG
  API --> VK[(Valkey)]
  BOT --> VK
  PG -->|"задача River"| W[worker]
  W -->|"HTTP-адаптер"| RW[Remnawave]
  W --> PG
  BOT -->|"уведомления операторам"| TG[Группа операторов]
```

Каждая точка входа сначала попадает в PostgreSQL. Только воркер обращается к Remnawave на пути записи, и делает
это из надёжной задачи, а не из запроса.

Покупка делает эту схему конкретной:

**1. Клиент выбирает тариф**

В браузере это `/account` и маршруты `/v1/account/checkout`; в Telegram — экраны каталога у бота. И то и
другое создаёт обычную строку заказа.

**2. Создаётся платёжное намерение**

Заказ получает намерение против одного адаптера провайдера. Клиента отправляют к провайдеру, либо он
платит в Telegram, либо заказ закрывается за счёт баланса кошелька.

**3. Провайдер присылает обратный вызов**

`POST /v1/payments/webhooks/{provider}` по необходимости не аутентифицирован и защищён проверкой подписи
по тем самым байтам, которые прислал провайдер. Повторное событие провайдера разрешается в
`{"status":"duplicate"}`, а не проводится вторично.

**4. Расчёт, право на услугу и задача коммитятся вместе**

В одной транзакции API помечает заказ оплаченным, записывает право на услугу, создаёт строку
`fulfillment_operations` с ключом идемпотентности `order:<orderID>:fulfill`, пишет событие `order.paid` в
outbox, начисляет реферальное вознаграждение, если оно есть, и вставляет задачу River.

**5. Воркер выдаёт доступ**

Воркер `fulfillment` забирает задачу, открывает сериализуемую транзакцию, блокирует строку операции и
вызывает Remnawave. При успехе предыдущие права на услугу этой подписки замещаются, а заказ переходит в
`fulfilled`.

**6. Клиент видит доступ**

Ссылка доступа читается из Remnawave вживую на каждый запрос. В Omniflow она никогда не сохраняется.

Если обратный вызов провайдера закоммитился, а вызов Remnawave затем упал, деньги записаны, право на услугу
существует, и есть задача с невыполненной работой. Именно ради этого отказа и проведена граница транзакции.
[Исполнение заказов](https://omniflow-vpn.mintlify.site/ru/commerce/fulfillment.md) описывает поведение при повторах, отложенных попытках и
расхождениях.

## Три префикса API — три модели доверия

HTTP API разделён на три префикса, которые никогда не используют общий маршрутизатор.

| Префикс | Учётные данные | Дополнительные проверки | Когда монтируется |
| - | - | - | - |
| `/v1/panel` | Cookie сессии `__Host-omniflow_admin` (`omniflow_admin` без TLS) | `X-CSRF-Token` на небезопасных методах, право доступа RBAC, объявленное при монтировании каждого маршрута, состояние TOTP-проверки, `X-Operator-Reason` на тех мутациях, где он обязателен | `APP_ADMIN_PANEL_ENABLED=true` и подключён административный сервис |
| `/v1/admin` | `Authorization: Bearer <APP_OPERATOR_TOKEN>` | Нет: ни CSRF, ни RBAC; изменения пишутся в аудит как `operator-token` | Всегда, как только собран коммерческий рантайм |
| `/v1/account` | Cookie сессии `__Host-omniflow_account` (`omniflow_account` без TLS) | `X-CSRF-Token` на небезопасных методах, повторная аутентификация (step-up) на необратимых действиях | `APP_CUSTOMER_PANEL_ENABLED=true` |

Объединение любых двух означало бы, что один молча наследует middleware другого, а учётные данные клиента
никогда не должны иметь возможность дотянуться до операторского маршрута. Структурное разделение делает это
свойством таблицы маршрутов, а не проверкой, о которой кто-то должен помнить; тест утверждает, что клиентский
маршрутизатор отвечает `404` на `/v1/panel/auth/session`, `/v1/admin/orders` и `/v1/panel/admins`.

Префикс, чей сервис не подключён, **отсутствует** — `404`, а не `503`, — потому что `503` сообщил бы анонимному
вызывающему, что настроил оператор. Исключение — маршруты до входа в систему, которые отвечают
`503 panel_unavailable` или `503 account_unavailable`, поскольку они работают до того, как появятся какие-либо
учётные данные.

Общий для процесса middleware применяется ко всем трём: идентификатор запроса, восстановление после паники,
60-секундный таймаут обработчика, OpenTelemetry и метрики. `/v1/admin` — это внешний интерфейс автоматизации:
ничто в этом репозитории его не вызывает, а бот общается с PostgreSQL и Remnawave внутри процесса, а не по
HTTP. См. [Интеграция с API](https://omniflow-vpn.mintlify.site/ru/integrations/api.md).

Граница веб-маршрутов организационная, а не защитная. Авторизация проверяется в Go API на каждом запросе, а
серверные компоненты `/admin` проверяют те же права доступа перед отрисовкой; отдельное имя хоста или скрытая
навигация никогда не являются достаточной авторизацией. Каталог прав доступа и сопоставление ролей
вкомпилированы в `internal/rbac`, поэтому панель отрисовывается ровно из того набора, который применяет API, и
правка в базе данных не может расширить доступ.

## Изменение состояния и его надёжная задача коммитятся вместе

Правило в `AGENTS.md` короткое: *ставить надёжные задачи River в очередь в той же транзакции PostgreSQL, что и
изменение состояния, которое их требует*. Именно это делает утверждение «у оплаченного заказа всегда есть
ожидающая его задача» истинным, а не желаемым, и существует интеграционный тест, весь смысл которого — это
свойство.

Хук постановки в очередь — это функция, передаваемая в коммерческое хранилище при старте процесса, поэтому один
и тот же код расчёта транзакционен и в API, и в воркере. Бот намеренно не передаёт хук: он создаёт заказы и
платёжные намерения, но выдача доступа принадлежит воркеру, а расчёт коммитит тот процесс, который получил
обратный вызов.

Повтор безопасен, потому что идентичность несёт надёжная запись, а не очередь. У `fulfillment_operations` есть
`UNIQUE` ключ идемпотентности, а создание строки выполняется как `ON CONFLICT (idempotency_key) DO UPDATE …
RETURNING`, поэтому повтор считывает уже существующую операцию. Повторное использование ключа с другими
параметрами отвергается сразу: *idempotency key was already used with different fulfillment parameters*.
Известные формы ключей: `order:<orderID>:fulfill`, `order:<orderID>:addon`, `gift:<giftID>:fulfill`,
`reconcile:<entitlementID>:<15-minute bucket>`, `panel:<subscriptionID>:<operation>:<requestID>` и
`bulk:<operationID>:<position>`.

Работу над отдельной записью несут два вида задач — `fulfillment` и `goods_delivery`, — обе на очереди
`critical` с `MaxAttempts: 12` и уникальностью по аргументам задачи; третий вид, `periodic_task`, несёт
[периодические проходы](#периодические-проходы-и-внутрипроцессные-циклы) на `default`. Настроенных очередей
три: `critical` (16 воркеров), `default` (8) и `bulk` (4). Воркеры зарегистрированы только в процессе worker;
API и бот создают клиенты River только для вставки.

> [!NOTE]
>
> Собственные таблицы River — `river_job`, `river_leader`, `river_queue`, `river_notification`,
> `river_migration` — создаёт закоммиченная миграция Atlas `20260905130000_river_queue.sql`, дословная копия
> собственной линии миграций River, поэтому сервис `migrate` в compose оставляет базу, на которой воркер
> может запуститься. См. [Задачи и очереди](https://omniflow-vpn.mintlify.site/ru/operations/jobs-and-queues.md#схема-очереди-поставляется-вместе-с-миграциями).

## Транзакционный outbox

Три топика записываются в `outbox_events` внутри той транзакции, которая их вызвала: `order.paid`,
`wallet.credited` и `addon.purchased`. Строка несёт топик и полезную нагрузку и записывается с той же гарантией
«всё или ничего», что и стоящее рядом изменение состояния.

Ничто их не рассылает. Ни один путь кода не выставляет `outbox_events.published_at`; единственные операторы,
которые трогают этот столбец, — два чтения с `WHERE published_at IS NULL` и удаление по сроку хранения, которое
отбирает `published_at IS NOT NULL`. На практике это означает, что outbox — надёжная запись о случившемся, а не
механизм доставки: `GET /v1/admin/outbox` сообщает `pendingCount`, который только растёт,
`APP_RETENTION_OUTBOX_DAYS` ничего не удаляет, а вкладка outbox в панели по адресу
`GET /v1/panel/system/outbox` показывает только топик и возраст — никогда полезную нагрузку, в которой могут
быть имя клиента и сумма.

## Периодические проходы и внутрипроцессные циклы

Каждый проход по всей таблице, меняющий состояние, — обслуживание заказов, кошельков и корзин, сверка
платежей, продления, планирование сверки и доставки, обходчик жизненного цикла, вычисление аномалий,
раскрытие кампаний, массовые операции, очистка по срокам хранения и резервные копии — это периодическая
задача River вида `periodic_task` на воркере. Ставит их в очередь только выбранный River лидер, и каждая
остаётся уникальной, пока стоит в очереди или выполняется, поэтому проход выполняется один раз за интервал
на всю установку, сколько бы воркеров ни работало. У прохода одна попытка: сбой записывается, а следующий
интервал запускает новый проход по тем же строкам.

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

## Поддерживаемая топология

Запускайте сколько угодно реплик `api` за прокси и сколько угодно реплик `worker` — и ровно один `bot`:
Telegram доставляет обновления одному потребителю. Каждой реплике воркера нужны общие каталоги резервных
копий и вложений, а пул подключений каждой реплики расходует `max_connections` PostgreSQL. См.
[Развёртывание](https://omniflow-vpn.mintlify.site/ru/operations/deployment.md#поддерживаемая-топология).

Стек compose не объявляет политику `restart:` ни для `postgres`, ни для `valkey`, `api`, `bot`, `worker` или
`web`; её задаёт только оверлей Traefik. Упавший процесс останется лежать, пока кто-нибудь не заметит, — именно
для этого нужны пробы и [Наблюдаемость](https://omniflow-vpn.mintlify.site/ru/operations/observability.md).

## Почему модульный монолит

`AGENTS.md` формулирует правило — *держать приложение на Go модульным монолитом с независимыми точками входа
`api`, `bot` и `worker`*, — и его удерживают четыре вспомогательных ограничения:

* Доменные пакеты не должны импортировать транспорт, генерацию базы данных, Telegram и пакеты конкретных
  провайдеров. Стрелки зависимостей направлены в одну сторону.
* Внешние системы скрыты за небольшими интерфейсами. К Remnawave обращаются только через `remnawave.Service`,
  `remnawave.Provisioner` и `remnawave.Importer`, поэтому все типы, повторяющие форму Remnawave, живут в одном
  пакете.
* Доменные события идут через транзакционный outbox, и брокер сообщений не вводится без принятого
  архитектурного решения.
* Построение зависимостей остаётся явным, без фреймворка внедрения зависимостей до тех пор, пока этого не
  потребует сложность связывания.

Что из этого получает оператор: одну базу данных, один набор миграций, один `.env` и три контейнера для
запуска. Что получает контрибьютор: изменение, затрагивающее оформление заказа, право на услугу и выдачу
доступа, — это одна транзакция и один тест, а не распределённый протокол. Это позволяет поддерживать сотни
независимых установок, не навязывая ни одной из них брокер, Kubernetes или среду выполнения плагинов.

## Куда двигаться дальше

> **[Владение данными](https://omniflow-vpn.mintlify.site/ru/architecture/data-ownership.md)**
>
> Точная граница с Remnawave: за что каждая система авторитетна и что ломается при нарушении границы.

> **[Технологии](https://omniflow-vpn.mintlify.site/ru/architecture/technology.md)**
>
> Каждый компонент стека и причина, по которой он там есть.

> **[Задачи и очереди](https://omniflow-vpn.mintlify.site/ru/operations/jobs-and-queues.md)**
>
> Виды задач, повторы, недоставленные задачи, циклы по расписанию и проходы очистки по срокам хранения.

> **[Развёртывание](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.