> ## 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 настраивается целиком через переменные окружения. Конфигурационного файла нет, флагов
командной строки нет — кроме `--decrypt-backup` у воркера, — и горячей перезагрузки тоже нет.
`internal/config` предоставляет три загрузчика: один для API, один для бота, один для воркера, и
каждый процесс выполняет свой загрузчик один раз, при старте. Значение, не прошедшее валидацию,
приводит к отказу сразу, а не остаётся скрытой проблемой: процесс пишет `invalid configuration` и
завершается с кодом 1, поэтому установка никогда не работает наполовину настроенной — с shop ID
YooKassa и без секрета или с включёнными резервными копиями и без ключа.

Следствие однократной загрузки в том, что **изменение переменной требует перезапуска процесса,
который её читает**. Docker Compose передаёт один и тот же файл `.env` сервисам `api`, `bot` и
`worker`, поэтому на практике один файл настраивает все три, а изменение означает повторный
`docker compose up -d`. Чтобы начать, скопируйте `.env.example` в `.env`; о первом запуске
см. [Быстрый старт](https://omniflow-vpn.mintlify.site/ru/getting-started/quickstart.md), о топологии compose —
[Развёртывание](https://omniflow-vpn.mintlify.site/ru/operations/deployment.md).

В таблицах ниже перечислены все переменные, которые читает хоть один процесс Omniflow, включая
несколько отсутствующих в `.env.example`, и одну, которую `.env.example` задаёт, но не читает никто.
**Кто читает** — это процессы, чей загрузчик читает переменную; о переменной, которую читает
процесс, никогда не использующий её значение, отдельно сказано в тексте под её таблицей.

## Как разбираются значения

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

| Вид | Относится к | Поведение при непригодном значении |
| - | - | - |
| Строка | Адреса, URL, токены, пути | Пустое значение равнозначно незаданному и приводит к откату на значение по умолчанию |
| Булево | Все `*_ENABLED`, `*_SECURE`, `*_TESTNET`, `*_AUTO_DETECT` | Разбирается через `strconv.ParseBool`; всё, что он отвергает, **молча** откатывается на значение по умолчанию |
| Длительность целым числом | `*_HOURS`, `*_DAYS`, `*_MINUTES`, `*_SECONDS` | Принимаются только целые числа, поэтому оператору никогда не приходится записывать длительность в формате Go. `1.5`, `30s` или отрицательное число молча откатываются на значение по умолчанию |
| Целочисленное количество | `*_CAP`, `*_STREAK`, `APP_MAX_SUBSCRIPTIONS_PER_CUSTOMER` | Отрицательное или неразбираемое значение откатывается на значение по умолчанию |
| Сумма в минорных единицах | `APP_WALLET_TOPUP_*_MINOR`, `APP_OPERATOR_CHAT_ID` | Неразбираемое значение откатывается; отрицательное принимается как написано |
| Список чисел | `APP_WALLET_TOPUP_PRESETS`, `APP_OPERATOR_TELEGRAM_IDS` | Элемент, не являющийся положительным целым числом, — это **ошибка запуска**, а не молчаливый пропуск, потому что иначе опечатка в пресете просто исчезла бы из бота без каких-либо объяснений |

> [!WARNING]
>
> Опечатка в булевом значении незаметна. `APP_METRICS_ENABLED=yes` и `APP_TELEMETRY_ENABLED=nope`
> оба отвергаются парсером и оба откатываются на значение по умолчанию, а оно в обоих случаях
> равно `true`. Используйте `true` и `false`.

## Что нужно каждому процессу для запуска

| Процесс | Обязательно | Если этого нет |
| - | - | - |
| `api` | Вообще ничего | С пустым `APP_DATABASE_URL` API поднимается и обслуживает только `/healthz`, `/livez`, `/readyz` и `/metrics`. Ни коммерческие маршруты, ни панель, ни личный кабинет не смонтированы, а `/readyz` отвечает `200` с пустым массивом `checks` — зелёный свет для процесса, который почти ничего не может обслужить |
| `api` с базой данных | `APP_OPERATOR_TOKEN`, 32-байтный `APP_DATA_ENCRYPTION_KEY`, `APP_REMNAWAVE_URL`, `APP_REMNAWAVE_TOKEN`, `APP_VALKEY_URL` | Выход с кодом 1 и сообщением `commerce requires APP_OPERATOR_TOKEN, APP_DATA_ENCRYPTION_KEY, APP_REMNAWAVE_URL, APP_REMNAWAVE_TOKEN, and APP_VALKEY_URL` |
| `bot` | `APP_TELEGRAM_TOKEN`, `APP_DATABASE_URL`, `APP_VALKEY_URL`, `APP_REMNAWAVE_URL`, `APP_REMNAWAVE_TOKEN` | С пустым `APP_TELEGRAM_TOKEN` процесс пишет `telegram bot disabled because APP_TELEGRAM_TOKEN is empty` и ждёт сигнала — код выхода 0, намеренно не ошибка. Если токен задан, но отсутствует другое значение, процесс завершается с кодом 1, называя переменную |
| `worker` | `APP_DATABASE_URL`, `APP_REMNAWAVE_URL`, `APP_REMNAWAVE_TOKEN` | Выход с кодом 1 и сообщением `APP_DATABASE_URL, APP_REMNAWAVE_URL, and APP_REMNAWAVE_TOKEN are required` |

Бот отказывается стартовать без Valkey, вместо того чтобы работать незащищённым: на Valkey держатся
его ограничения частоты запросов по пользователю и его защита от повторного проигрывания callback,
и обе стоят между двойным нажатием и двойным списанием. Ошибка говорит об этом прямо —
`APP_VALKEY_URL is required for bot rate limits and callback replay protection`.

## Основное

| Переменная | По умолчанию | Кто читает | Обязательна | Секрет | Что меняет |
| - | - | - | - | - | - |
| `APP_HTTP_ADDR` | `:8080` | api | нет | нет | Адрес прослушивания API. Compose также задаёт его явно на сервисе `api` |
| `APP_OPERATOR_TOKEN` | — | api | да, когда у API есть база данных | **да** | Bearer-токен для всего префикса `/v1/admin`, сравнивается за постоянное время. См. [Интеграция с API](https://omniflow-vpn.mintlify.site/ru/integrations/api.md) |
| `APP_DATA_ENCRYPTION_KEY` | — | api, bot, worker | для api да при наличии базы; для bot и worker необязательна | **да** | AES-ключ в base64 длиной 32 байта. Должен декодироваться ровно в 32 байта, иначе запуск падает с `APP_DATA_ENCRYPTION_KEY must be base64-encoded 32 bytes` |
| `APP_ENV` | — | никто | нет | нет | Присутствует в `.env.example` со значением `development`. Ни один код на Go её не читает, и её установка ничего не меняет |

`APP_DATA_ENCRYPTION_KEY` — это ключ, которым запечатываются контактные данные клиентов, TOTP-секреты
операторов, сохранённые в панели настройки и секреты OIDC, cookie процесса входа клиента, учётные
данные провайдеров цифровых товаров и отпечатки контактов реферальной программы. Сгенерируйте его
один раз:

```bash theme={null}
openssl rand -base64 32
```

> [!WARNING]
>
> Сохраняйте резервную копию ключа вместе с базой данных и никогда не меняйте его без веской
> причины. Смена делает нечитаемым каждое значение, уже запечатанное старым ключом, и ни один
> процесс не предложит их перешифровать.

Бот и воркер каждый запускается без него и отключает одну возможность, вместо того чтобы отказаться
работать. Воркер без ключа пишет `digital goods delivery disabled` с
`reason: APP_DATA_ENCRYPTION_KEY is not set`, а кроме того работает без обновления блок-листа и
оценки аномалий в свипере и без исполнителя массовых действий, потому что все три распечатывают
учётные данные, записанные панелью. Задайте ключ на воркере, как только начнёте продавать цифровые
товары, подпишетесь на блок-лист или воспользуетесь массовыми действиями.

## База данных и кеш

| Переменная | По умолчанию | Кто читает | Обязательна | Секрет | Что меняет |
| - | - | - | - | - | - |
| `APP_DATABASE_URL` | — | api, bot, worker | для bot и worker да; для api необязательна | **да** (содержит пароль) | Строка подключения pgx. На API пустое значение отключает всю коммерческую поверхность, панель и личный кабинет |
| `APP_DATABASE_MAX_CONNS` | api `20`, bot `10`, worker `40` | api, bot, worker | нет | нет | Размер пула PostgreSQL процесса. Всё, кроме целого положительного числа, валит запуск, а воркер не принимает меньше `32`. Параметр `pool_max_conns` в `APP_DATABASE_URL` имеет приоритет. Чтобы задать процессам разные размеры, укажите переменную для каждого сервиса — см. [Развёртывание](https://omniflow-vpn.mintlify.site/ru/operations/deployment.md#подключения-к-базе-данных) |
| `APP_VALKEY_URL` | — | api, bot, worker | для api да при наличии базы; для bot да | нет | Обеспечивает ограничения частоты запросов, защиту от повторов, блокировки и временное состояние |

Служба резервного копирования разбирает `APP_DATABASE_URL` второй раз — в `PGHOST`, `PGPORT`,
`PGDATABASE`, `PGUSER`, `PGPASSWORD` и `PGSSLMODE`, — чтобы пароль попадал в `pg_dump` через
окружение, а не через argv дочернего процесса, где его мог бы прочитать любой локальный пользователь.

Загрузчик воркера читает `APP_VALKEY_URL`, а сам воркер ничего из этого не использует: ни один путь
исполнения в его коде не открывает клиент Valkey. Оставить переменную заданной безвредно, и это
позволяет держать один `.env` пригодным для всех трёх процессов.

> [!WARNING]
>
> Compose-сервис `migrate` жёстко зашивает
> `postgres://omniflow:omniflow@postgres:5432/omniflow?sslmode=disable` и не читает `.env`.
> Смена учётных данных базы данных означает правку и `compose.yaml`.

## Remnawave

| Переменная | По умолчанию | Кто читает | Обязательна | Секрет | Что меняет |
| - | - | - | - | - | - |
| `APP_REMNAWAVE_URL` | — | api, bot, worker | да | нет | Базовый URL API Remnawave |
| `APP_REMNAWAVE_TOKEN` | — | api, bot, worker | да | **да** | Учётные данные API Remnawave |

Проба готовности API обращается к Remnawave аутентифицированным запросом, который перечисляет одного
пользователя, поэтому отозванный или истёкший токен проявляется как нездоровая зависимость
`remnawave` в `/readyz`, а не как сорвавшаяся покупка позже. Три подряд нездоровых замера по
умолчанию также открывают режим обслуживания. См. [Интеграция с Remnawave](https://omniflow-vpn.mintlify.site/ru/integrations/remnawave.md).

## Telegram

| Переменная | По умолчанию | Кто читает | Обязательна | Секрет | Что меняет |
| - | - | - | - | - | - |
| `APP_TELEGRAM_TOKEN` | — | api, bot, worker | для bot да | **да** | Токен Bot API. На API он включает платёжный адаптер Telegram Stars и проверку полезной нагрузки login-виджета и Mini App; на воркере — адаптер Stars для автоматических продлений |
| `APP_TELEGRAM_WEBHOOK_URL` | — (длинные опросы) | bot | нет | нет | Переключает бота в режим webhook. Должен быть HTTPS-URL, иначе запуск падает |
| `APP_TELEGRAM_WEBHOOK_SECRET` | — | bot | вместе с URL | **да** | Должен состоять из 32–256 символов `A–Z a–z 0–9 _ -`, иначе запуск падает. Telegram присылает его обратно с каждым обновлением, и библиотека отвергает обновление без него ещё до разбора |
| `APP_BOT_HTTP_ADDR` | `:8082` | bot | нет | нет | Адрес приёмника webhook. Эндпоинт — `POST /telegram/webhook` с ограничением тела в 1 MiB |
| `APP_SUPPORT_URL` | — | bot | нет | нет | Ссылка на чат поддержки или хелпдеск, которую показывает бот. Если задана, валидируется как `http`/`https` URL |

Длинные опросы используются по умолчанию и остаются поддерживаемым режимом для разработки и как
запасной вариант. Установка webhook-URL заставляет бота при старте вызвать `setWebhook` с
`allowed_updates`, ограниченным значениями `message`, `edited_message`, `callback_query`,
`pre_checkout_query`, `my_chat_member` и `chat_member`. Сгенерируйте секрет командой
`openssl rand -hex 32`. Полная настройка, включая BotFather и домен Mini App, описана в
[Интеграции с Telegram](https://omniflow-vpn.mintlify.site/ru/integrations/telegram.md).

## URL и валюта

| Переменная | По умолчанию | Кто читает | Обязательна | Секрет | Что меняет |
| - | - | - | - | - | - |
| `APP_PUBLIC_URL` | — | api, bot | для YooKassa и для ссылки входа | нет | Базовый URL для возвратных ссылок провайдеров, реферальных ссылок, абсолютной ссылки входа, которую отправляет бот, и origin, к которому привязываются passkey операторов |
| `APP_TERMS_URL` | — | api, bot | нет | нет | Ссылка на условия на экранах тарифа и оформления заказа. Если задана, валидируется как `http`/`https` URL |
| `APP_UPDATE_FEED_URL` | — | api | нет | нет | Лента релизов, с которой диагностический пакет сравнивает работающую версию. Пусто — проверки нет и запроса нет. Если задана, валидируется как `http`/`https` URL |
| `APP_AI_ALLOW_PRIVATE_NETWORK` | `false` | api | нет | нет | Разрешает базовому URL AI-провайдера обычный HTTP и обращения к loopback, частным и link-local адресам — это нужно модели в том же стеке Compose или в локальной сети. Когда выключено, базовый URL должен быть HTTPS к публичному адресу. См. [AI-провайдеры](https://omniflow-vpn.mintlify.site/ru/operations/ai-providers.md#подключение-локальной-модели) |

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

Укажите любой JSON-документ, называющий версию: эндпоинт релизов GitHub отвечает полем `tag_name`, а
владелец, публикующий собственный файл, может отвечать полем `version`. Читаются обе формы. Результат
появляется в диагностическом пакете под ключом `update`, и лента читается не чаще раза в шесть часов,
сколько бы раз пакет ни собирали.

Лента, которая отказала, не ответила вовремя или вернула нечитаемое, сообщает `unreachable`, а не «всё
актуально». Подробность — это категория, и никогда не адрес: в URL ленты может быть токен.
\| `APP_DEFAULT_CURRENCY` | `RUB` | api, bot, worker | нет | нет | Валюта расчётов, в которой каталог сравнивает тарифы. Приводится к верхнему регистру, затем сверяется с `^[A-Z]{3}$`; всё остальное валит запуск |

От `APP_PUBLIC_URL` зависят два отказа при запуске, и оба существуют потому, что альтернатива —
ссылка, ведущая в никуда: бот отказывается стартовать, когда YooKassa настроена, а публичного URL
нет (`APP_PUBLIC_URL is required for YooKassa hosted checkout return links`), и бот, и API
отказываются, когда `APP_CUSTOMER_MAGIC_LINK_ENABLED=true`, а публичный URL не задан.

Passkey операторов — третье, что от него зависит, и единственное, что при запуске **не** приводит к
отказу. Passkey привязан к origin, поэтому без публичного URL привязывать не к чему и функция просто
выключена: экран входа не показывает кнопку, вместо того чтобы показать кнопку, которая ничего не
открывает. Планировать нужно обратное: смена этого значения после того, как операторы
зарегистрировали ключи, делает недействительными все ключи — браузер не предложит учётные данные
сайту, для которого они не создавались. См. [Безопасность](https://omniflow-vpn.mintlify.site/ru/operations/security.md).

Используйте `XTR` для установки, продающей только через Telegram Stars. Тариф можно купить лишь
способом оплаты, который рассчитывается в одной из валют, в которых у тарифа есть цена, поэтому
назначайте тарифу цену в `XTR`, чтобы продавать его через Stars, и в фиатной валюте, чтобы
продавать через CryptoBot или YooKassa. См. [Каталог и промоакции](https://omniflow-vpn.mintlify.site/ru/commerce/catalog-promotions.md).

## Платёжные провайдеры

| Переменная | По умолчанию | Кто читает | Обязательна | Секрет | Что меняет |
| - | - | - | - | - | - |
| `APP_CRYPTOBOT_TOKEN` | — | api, bot, worker | нет | **да** | Включает адаптер CryptoBot, когда не пуста |
| `APP_CRYPTOBOT_TESTNET` | `true` | api, bot, worker | нет | нет | Выбирает тестовую сеть CryptoBot вместо основной |
| `APP_YOOKASSA_SHOP_ID` | — | api, bot, worker | нет | нет | Включает YooKassa |
| `APP_YOOKASSA_SECRET` | — | api, bot, worker | нет | **да** | Секрет Basic-Auth для YooKassa |

Адаптер существует только тогда, когда настроены его учётные данные, поэтому отдельного
переключателя включения нет. Пару значений YooKassa нужно задавать вместе: бот отказывается
стартовать с одним без другого (`APP_YOOKASSA_SHOP_ID and APP_YOOKASSA_SECRET must be configured
together`), потому что shop ID без секрета — это установка, предлагающая способ оплаты, который она
не может довести до конца.

Воркер держит те же учётные данные, что и API, потому что именно он списывает деньги за
автоматические продления: воркер без адаптера мог бы разве что записать, что продление не удалось.
Настройка каждого провайдера — в [Платёжных провайдерах](https://omniflow-vpn.mintlify.site/ru/integrations/payments.md), а сценарий
регулярных списаний — в [Регулярных платежах](https://omniflow-vpn.mintlify.site/ru/commerce/recurring-payments.md).

## Пополнение кошелька и лимиты подписок

| Переменная | По умолчанию | Кто читает | Обязательна | Секрет | Что меняет |
| - | - | - | - | - | - |
| `APP_WALLET_TOPUP_ENABLED` | `true` | api, bot, worker | нет | нет | Предлагать пополнение по инициативе клиента |
| `APP_WALLET_TOPUP_PRESETS` | `10000,30000,50000,100000` | api, bot, worker | нет | нет | Суммы в одно нажатие, в минорных единицах. Неположительный или неразбираемый элемент валит запуск |
| `APP_WALLET_TOPUP_MINIMUM_MINOR` | `10000` | api, bot, worker | нет | нет | Наименьшее одиночное пополнение. Не должно превышать максимум, когда максимум больше нуля |
| `APP_WALLET_TOPUP_MAXIMUM_MINOR` | `5000000` | api, bot, worker | нет | нет | Наибольшее одиночное пополнение; `0` снимает верхнюю границу |
| `APP_WALLET_TOPUP_WINDOW_LIMIT_MINOR` | `10000000` | api, bot, worker | нет | нет | Сколько разрешено зачислить внутри скользящего окна |
| `APP_WALLET_TOPUP_WINDOW_HOURS` | `24` | api, bot, worker | нет | нет | Длина скользящего окна |
| `APP_MULTI_SUBSCRIPTION_ENABLED` | `false` | api, bot, worker | нет | нет | Разрешить одному клиенту держать несколько одновременных подписок |
| `APP_MAX_SUBSCRIPTIONS_PER_CUSTOMER` | `3` | api, bot, worker | нет | нет | Потолок установки, пока включены мультиподписки. Значение меньше 1 поднимается до 1 |

Суммы указываются в минорных единицах `APP_DEFAULT_CURRENCY`. Пресет, который лимиты отвергли бы,
вообще не отрисовывается, поэтому клиент не может нажать кнопку, которую бэкенд затем отклонит.

> [!NOTE]
>
> Эти переменные — то, что применяют работающие процессы. При первом запуске против базы данных, где
> нет строки `commerce_settings`, API дополнительно заполняет эту строку из этих же значений, чтобы
> установка, обновляющаяся с более старой версии, сохранила лимиты, настроенные её оператором, а не
> молча взяла значения по умолчанию из схемы. Дальше строку пишет только панель
> (`PUT /v1/panel/settings/commerce/topup` и `.../subscriptions`), и именно сохранённая строка
> показывается в панели и в обзоре клиентского веб-кабинета — но заказы и кошелёк по-прежнему
> проверяются по значениям окружения, загруженным при старте. Держите переменные заданными и
> согласованными с панелью и перезапускайте процессы после изменения любой из сторон.

Заполнение никогда не блокирует запуск: сбой пишется в лог как
`commerce settings could not be seeded; the panel will show none yet`, и API продолжает работу. См.
[Пополнение кошелька и корзины](https://omniflow-vpn.mintlify.site/ru/commerce/wallet-topup-cart.md) и
[Подписки](https://omniflow-vpn.mintlify.site/ru/commerce/subscriptions.md).

## Коммуникация с клиентами

| Переменная | По умолчанию | Кто читает | Обязательна | Секрет | Что меняет |
| - | - | - | - | - | - |
| `APP_MARKETING_FREQUENCY_CAP` | `3` | bot | нет | нет | Сколько маркетинговых сообщений разрешено за окно; `0` отключает ограничение |
| `APP_MARKETING_WINDOW_HOURS` | `168` | bot | нет | нет | Длина маркетингового окна — семь дней по умолчанию |
| `APP_TELEGRAM_DELIVERY_RATE` | `25` | bot | нет | нет | Сколько сообщений в секунду бот отправляет во все чаты вместе; потолок Telegram для одного бота — около 30. `0` — значение по умолчанию |
| `APP_RECOVERY_WINDOW_HOURS` | `336` | api, bot | нет | нет | Сколько истёкшая подписка сохраняет оформление заказа на восстановление в одно нажатие — четырнадцать дней по умолчанию |
| `APP_TRIAL_MINIMUM_ACCOUNT_AGE_HOURS` | `0` | api, bot | нет | нет | Отказывать в пробном периоде аккаунтам Telegram моложе этого возраста |
| `APP_CART_TTL_HOURS` | `720` | bot | нет | нет | Сколько сохранённая корзина ждёт, пока баланса хватит на её оплату, — тридцать дней по умолчанию |

Окно восстановления и контроль возраста аккаунта для пробного периода читает не только бот, но и API,
и это сделано намеренно: клиентский веб-кабинет оформляет заказ по тем же правилам, что и бот, а
пробный период, в котором чат отказывает, а браузер выдаёт, был бы средством контроля с дырой. Задавайте
их одинаково для обоих процессов — один общий `.env` делает это сам. Маркетинговые ограничения и время
жизни корзины читает только бот, потому что именно он отправляет сообщения и именно он держит
сохранённые корзины. Поведение описано в [Уведомлениях и согласиях](https://omniflow-vpn.mintlify.site/ru/customer/notifications.md).

## Панель оператора, клиентская панель и сессии

| Переменная | По умолчанию | Кто читает | Обязательна | Секрет | Что меняет |
| - | - | - | - | - | - |
| `APP_ADMIN_PANEL_ENABLED` | `true` | api | нет | нет | Монтирует `/v1/panel`. Отключите для установки, работающей только через бота |
| `APP_ADMIN_COOKIE_SECURE` | `true` | api | нет | нет | Флаг `Secure` на cookie сессии оператора |
| `APP_ADMIN_TOTP_ISSUER` | `Omniflow` | api | нет | нет | Подпись, которую учётные записи операторов получают внутри приложения-аутентификатора |
| `APP_CUSTOMER_PANEL_ENABLED` | `true` | api | нет | нет | Монтирует `/v1/account`. Отключите для установки, работающей только через Telegram |
| `APP_CUSTOMER_COOKIE_SECURE` | `true` | api | нет | нет | Флаг `Secure` на cookie сессии клиента |
| `APP_CUSTOMER_MAGIC_LINK_ENABLED` | `false` | api, bot | нет | нет | Включает ссылку для входа, доставляемую ботом, и команду бота `/login`. Требует `APP_PUBLIC_URL` |
| `APP_TRUSTED_PROXIES` | — | api | нет | нет | IP-адреса или CIDR-блоки через запятую, чьему `X-Forwarded-For` можно верить |

Оба флага cookie по умолчанию равны `true`, поэтому развёртывание, которое о них забыло, всё равно
получает cookie, которую браузер отправит только по TLS. Установка любого из них в `false` пишет в
лог предупреждение, называющее риск, —
`admin session cookie is not marked Secure; use this only for local development`.

> [!WARNING]
>
> `APP_ADMIN_COOKIE_SECURE=false` и `APP_CUSTOMER_COOKIE_SECURE=false` предназначены только для
> локального стека по обычному HTTP. Без `Secure` cookie сессии можно перехватить из любого
> незашифрованного запроса к тому же хосту.

Ссылка для входа выключена по умолчанию, потому что это вторые предъявительские учётные данные,
путешествующие через чат. Включайте её там, где нельзя воспользоваться login-виджетом Telegram —
обычно из-за того, что домен не удаётся привязать в BotFather, — и команда бота `/login` начинает
работать. Самому входу через Telegram отдельная переменная не нужна: он проверяет полезную нагрузку
виджета с помощью `APP_TELEGRAM_TOKEN`.

`APP_TRUSTED_PROXIES` разбирается один раз при старте и используется и операторской, и клиентской
поверхностью. Некорректный элемент — это ошибка запуска (`trusted proxy entry must be an IP address
or CIDR block`); голый IP превращается в префикс `/32` или `/128`. Заголовок `X-Forwarded-For`
подделывается тривиально, поэтому он учитывается только тогда, когда непосредственный узел-отправитель
— один из настроенных прокси, а если не настроен ни один, заголовок игнорируется полностью и
используется адрес сокета: правильное поведение по умолчанию для развёртывания, выставленного напрямую.
Полученный адрес питает ограничение частоты запросов, журнал событий безопасности клиента и журнал
аудита, поэтому важны обе стороны ошибки: не доверять никому — и все запросы за прокси делят один
адрес; доверять слишком многим — и атакующий сам выбирает себе адрес. Для мостовой сети Docker за
обратным прокси обычное значение — `172.16.0.0/12`. См.
[Обратный прокси](https://omniflow-vpn.mintlify.site/ru/operations/reverse-proxy.md), [Панель администратора](https://omniflow-vpn.mintlify.site/ru/operations/admin-panel.md)
и [Клиентский веб-кабинет](https://omniflow-vpn.mintlify.site/ru/customer/web-panel.md).

## Уведомления операторам и поверхность `/ops`

| Переменная | По умолчанию | Кто читает | Обязательна | Секрет | Что меняет |
| - | - | - | - | - | - |
| `APP_OPERATOR_CHAT_ID` | `0` (отключено) | bot | нет | нет | Группа Telegram с включёнными темами, которая получает уведомления для операторов. Бот сам создаёт и привязывает каждую тему |
| `APP_OPERATOR_TELEGRAM_IDS` | — (пустое значение отключает `/ops`) | bot | для резервного копирования и восстановления из Telegram | нет | Telegram-идентификаторы пользователей через запятую, которым разрешено выполнять `/ops`. Ошибочный элемент валит запуск |
| `APP_OPERATOR_NOTIFICATION_CAP` | `30` | bot | нет | нет | Сколько сообщений каждого вида проходит за окно, прежде чем остальные записываются как `suppressed` с `error_code = volume_cap` |
| `APP_OPERATOR_NOTIFICATION_WINDOW_MINUTES` | `5` | bot | нет | нет | Длина окна подсчёта объёма |

Если `APP_OPERATOR_CHAT_ID` остаётся равным `0`, нотификатор пишет `operator notifications are
disabled because no group is configured`, а постановка в очередь становится пустой операцией, так
что уведомления даже не сохраняются. Команда `/ops` регистрируется безусловно и просто ничего не
отвечает Telegram-аккаунту, которого нет в списке, поэтому существование команды никогда не
подтверждается клиенту; кроме того, она отвечает только в личном чате. См.
[Уведомления операторам](https://omniflow-vpn.mintlify.site/ru/operations/operator-notifications.md).

## Резервные копии

| Переменная | По умолчанию | Кто читает | Обязательна | Секрет | Что меняет |
| - | - | - | - | - | - |
| `APP_BACKUP_ENABLED` | `false` | bot, worker | нет | нет | Резервные копии по расписанию на воркере и состояние включённости поверхности `/ops` в боте |
| `APP_BACKUP_ENCRYPTION_KEY` | — | bot, worker | при включённых резервных копиях | **да** | AES-GCM-ключ в base64 длиной 32 байта, отдельный от `APP_DATA_ENCRYPTION_KEY`. Запуск падает с `APP_BACKUP_ENCRYPTION_KEY is required when APP_BACKUP_ENABLED is true` |
| `APP_BACKUP_DIR` | `/var/lib/omniflow/backups` | bot, worker | нет | нет | Куда пишутся зашифрованные дампы. Каталог создаётся с режимом `0700`, файлы — с `0600` |
| `APP_BACKUP_INTERVAL_HOURS` | `24` | bot, worker | нет | нет | Интервал резервного копирования по расписанию; расписание ведёт только воркер |
| `APP_BACKUP_RETENTION_DAYS` | `14` | bot, worker | нет | нет | Сколько завершённый файл копии хранится, прежде чем его удалит подчистка |
| `APP_PG_DUMP_PATH` | `pg_dump` | bot, worker | нет | нет | Переопределение, когда клиентские утилиты не лежат в `PATH` |
| `APP_PG_RESTORE_PATH` | `pg_restore` | bot, worker | нет | нет | Переопределение для восстановления |

Образы бота и воркера собираются с включёнными клиентскими утилитами PostgreSQL, и оба монтируют
один и тот же том `backup-data` в `/var/lib/omniflow/backups`, поэтому под compose значения по
умолчанию работают без изменений. Тот же ключ читается и напрямую — до всякой загрузки конфигурации —
командой `worker --decrypt-backup`, которая превращает зашифрованный дамп обратно во входные данные
для `pg_restore`, не используя ничего, кроме бинарника, ключа и файла.

> [!WARNING]
>
> Храните `APP_BACKUP_ENCRYPTION_KEY` там, куда не достаёт сама резервная копия базы данных. Копию,
> зашифрованную ключом, который существует только внутри этой копии, восстановить невозможно.

См. [Резервное копирование и восстановление](https://omniflow-vpn.mintlify.site/ru/operations/backup-restore.md).

## Обнаружение режима обслуживания

| Переменная | По умолчанию | Кто читает | Обязательна | Секрет | Что меняет |
| - | - | - | - | - | - |
| `APP_MAINTENANCE_AUTO_DETECT` | `true` | api, worker | нет | нет | Автоматически входить в режим обслуживания и выходить из него по сериям состояний зависимостей |
| `APP_MAINTENANCE_PROBE_SECONDS` | `30` | api, worker | нет | нет | Интервал между замерами зависимостей; неположительное значение становится 30 секундами |
| `APP_MAINTENANCE_FAILURE_STREAK` | `3` | api, worker | нет | нет | Сколько подряд нездоровых замеров нужно, чтобы покупки приостановились; меньше 1 поднимается до 1 |
| `APP_MAINTENANCE_RECOVERY_STREAK` | `3` | api, worker | нет | нет | Сколько подряд здоровых замеров нужно, чтобы автоматически открытое окно закрылось |

Действует по ним только API. Это процесс, который и без того опрашивает каждую зависимость для
готовности, поэтому именно он ведёт контроллер; загрузчик воркера разбирает те же четыре значения и
не использует ни одного. Серии существуют, чтобы один таймаут не мог закрыть установку, а окно,
открытое оператором вручную, никогда не снимается автоматически. Автоматическое обнаружение следит
только за `postgres` и `remnawave` — отказ Valkey виден в `/readyz` и сам по себе режим обслуживания
никогда не открывает. См. [Режим обслуживания](https://omniflow-vpn.mintlify.site/ru/operations/maintenance-mode.md).

## Сроки хранения и вложения поддержки

| Переменная | По умолчанию | Кто читает | Обязательна | Секрет | Что меняет |
| - | - | - | - | - | - |
| `APP_RETENTION_OUTBOX_DAYS` | `7` | worker | нет | нет | Удаляет строки `outbox_events`, которые опубликованы и старше этого срока |
| `APP_RETENTION_TELEMETRY_DAYS` | `30` | worker | нет | нет | Удаляет собранные `telemetry_events` по времени получения |
| `APP_RETENTION_DRIFT_DAYS` | `90` | worker | нет | нет | Удаляет `entitlement_drifts` не в открытом состоянии — по времени разрешения |
| `APP_RETENTION_INTERVAL_HOURS` | `1` | worker | нет | нет | Интервал подчистки; неположительное значение становится одним часом |
| `APP_SUPPORT_ATTACHMENT_DIR` | `/var/lib/omniflow/support-attachments` | api, worker | нет | нет | Куда сохраняется файл, загруженный через клиентский веб-кабинет, и где воркер ищет его, чтобы освободить место |

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

Окно outbox сейчас не удаляет ничего. Omniflow пишет строки `outbox_events` для `order.paid`,
`wallet.credited` и `addon.purchased`, но ни один процесс не помечает их опубликованными, а запрос
на удаление совпадает только с опубликованными строками. Ожидайте, что outbox будет расти, а
`GET /v1/admin/outbox` — показывать растущее число ожидающих; см.
[Задания и очереди](https://omniflow-vpn.mintlify.site/ru/operations/jobs-and-queues.md).

> [!WARNING]
>
> `APP_SUPPORT_ATTACHMENT_DIR` отсутствует в `.env.example`, и `compose.yaml` не монтирует под неё
> том ни на сервисе `api`, ни на `worker`. Поэтому вложение к обращению, загруженное из браузера,
> живёт в записываемом слое контейнера API и теряется при пересоздании контейнера, а воркер —
> нацеленный на собственную пустую копию того же пути — удаляет строки по сроку хранения, пока
> уцелевшие байты остаются там, где были. Если вы принимаете загрузки из браузера, задайте
> переменную с одним и тем же путём на обоих процессах и смонтируйте туда один общий том.

Вложений, загруженных через Telegram, это не касается: их байты остаются у Telegram, а Omniflow
хранит только ссылку. См. [Служба поддержки](https://omniflow-vpn.mintlify.site/ru/operations/support-desk.md).

## Анонимная телеметрия

| Переменная | По умолчанию | Кто читает | Обязательна | Секрет | Что меняет |
| - | - | - | - | - | - |
| `APP_TELEMETRY_ENABLED` | `true` | api | нет | нет | Запускается ли вообще горутина ежедневного heartbeat |
| `APP_TELEMETRY_ENDPOINT` | `https://telemetry.omniflw.xyz/v1/telemetry/events` | api | нет | нет | URL коллектора. Пока телеметрия включена, он должен разбираться и быть HTTPS, иначе запуск падает с `APP_TELEMETRY_ENDPOINT must be an HTTPS URL` |
| `APP_TELEMETRY_COLLECTOR_ENABLED` | `false` | api | нет | нет | Монтирует `POST /v1/telemetry/events` на этой установке и запускает 180-дневный цикл хранения коллектора |

Телеметрия включена по умолчанию, и отправляет её только API — ни бот, ни воркер клиента не создают.
`APP_TELEMETRY_ENABLED=false` предотвращает все сетевые запросы телеметрии, а при старте API пишет
либо `anonymous telemetry enabled` с `disable_with=APP_TELEMETRY_ENABLED=false`, либо
`anonymous telemetry disabled`.

> [!NOTE]
>
> Предпросмотр телеметрии в панели, в разделе **Настройки**, читает сохранённый документ настроек, а
> не работающую конфигурацию, поэтому установка по умолчанию показывает предпросмотр как
> отключённый, хотя API на самом деле отправляет heartbeat. Считайте `APP_TELEMETRY_ENABLED` ответом
> на вопрос, включена ли телеметрия.

Точный состав отправляемых данных — на странице [Телеметрия](https://omniflow-vpn.mintlify.site/ru/operations/telemetry.md).

## Операционные эндпоинты

| Переменная | По умолчанию | Кто читает | Обязательна | Секрет | Что меняет |
| - | - | - | - | - | - |
| `APP_METRICS_ENABLED` | `true` | api, worker | нет | нет | Создаётся ли реестр Prometheus. `false` убирает маршрут `/metrics` и HTTP-мидлвар метрик на этом процессе |
| `APP_METRICS_TOKEN` | — | api | нет | **да** | Если задан, `GET /metrics` на слушателе API отвечает только на `Authorization: Bearer <token>`, иначе `401`. Пусто — маршрут открыт, а при старте пишется предупреждение; в этом случае закройте его на прокси. Воркер и бот отдают метрики на собственных внутренних слушателях и эту переменную не читают |
| `APP_WORKER_HTTP_ADDR` | `:8081` | worker | нет | нет | Приёмник `/livez`, `/readyz`, `/healthz` и `/metrics` у воркера |
| `APP_BOT_METRICS_ADDR` | — (пустое значение означает отсутствие HTTP) | bot | нет | нет | Приёмник `/livez`, `/readyz`, `/healthz` и `/metrics` у бота |

Три детали расходятся с тем, что подсказывают имена переменных, и каждая уже ловила операторов:

* **Бот игнорирует `APP_METRICS_ENABLED`.** Он создаёт реестр метрик безусловно и обслуживает
  `/metrics` всякий раз, когда задан `APP_BOT_METRICS_ADDR`.
* **`APP_BOT_METRICS_ADDR` игнорируется, когда совпадает с `APP_BOT_HTTP_ADDR`.** Отдавать метрики и
  готовность на смотрящем в интернет приёмнике webhook значило бы публиковать внутренности установки
  на хосте, который должен быть доступен откуда угодно, поэтому бот вместо этого отказывается.
* **Очистка `APP_WORKER_HTTP_ADDR` не отключает приёмник воркера.** Пустое значение считается
  незаданным и откатывается на `:8081`. Вместо этого держите порт вне публичной сети средствами
  прокси или файрвола.

У бота из коробки нет эндпоинта проб, потому что `APP_BOT_METRICS_ADDR` по умолчанию не задан.
Задайте его, если хотите `/readyz` на всех трёх процессах. Оба поставляемых примера обратного прокси
возвращают 404 для `/metrics` и `/readyz` на публичном хосте API. См.
[Наблюдаемость](https://omniflow-vpn.mintlify.site/ru/operations/observability.md).

## Трассировка

Omniflow инструментирует входящий HTTP, обновления Telegram, задания исполнения заказов, запросы к
PostgreSQL и исходящий HTTP спанами OpenTelemetry, но **ни один процесс Omniflow не устанавливает
провайдер трассировки, и ни одна переменная `OTEL_*` нигде в коде не читается**. Когда провайдер не
установлен, глобальный провайдер является пустышкой, поэтому установка `OTEL_EXPORTER_OTLP_ENDPOINT`
или любой другой стандартной переменной SDK сегодня ничего не меняет. Настройки, которая включает
трассировку, не существует. Поддерживаемые операционные сигналы — метрики и структурированные логи.

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

Приложение Next.js в `apps/web` читает собственные переменные. Они задаются на сервисе `web` в
`compose.yaml`, а не в `.env`.

| Переменная | По умолчанию | Кто читает | Обязательна | Секрет | Что меняет |
| - | - | - | - | - | - |
| `NEXT_PUBLIC_API_URL` | — (compose задаёт `http://api:8080`) | web | нет | нет | Запасной базовый URL для серверной проверки сессии панели |
| `API_INTERNAL_URL` | — | web | нет | нет | Адрес, по которому веб-приложение обращается к API: серверный запрос сессии и те запросы `/v1`, которые браузер делает на том же origin, — `proxy.ts` направляет их туда, если раньше на них не ответил обратный прокси. Откатывается на `NEXT_PUBLIC_API_URL`, а затем на пустую строку; в этом случае `/v1` отвечает 502 с проблемным документом, называющим эту переменную. Отсутствует в `.env.example` |
| `PLAYWRIGHT_BASE_URL` | — | только сквозные тесты | нет | нет | Цель набора тестов Playwright |
| `CI` | — | только сквозные тесты | нет | нет | Переключает конфигурацию Playwright на настройки для CI |

Запросы из браузера по умолчанию идут на тот же origin; развёртывание с другим origin задаёт базовый
URL клиента явно, а не через переменную окружения.

## Настройки, которые панель хранит, но не применяет

Панель оператора пишет документ `installation_settings` для каждого раздела — бренд, Remnawave, бот,
группа операторов, обслуживание, уведомления, телеметрия, резервные копии, политика сессий. Эти
документы читаются обратно только собственным экраном настроек панели и предпросмотром телеметрии.
**Ни один работающий процесс не читает документ раздела или его запечатанные секреты**, поэтому
сохранение документа не меняет никакого поведения: URL и токен Remnawave, токен бота, группа
операторов, политика обслуживания, включённость телеметрии и расписание резервных копий берутся из
переменных окружения выше, прочитанных один раз при старте. Считайте `.env` источником истины, а
экран настроек — записью намерений, пока это не изменится. Экран и эту границу описывают
[Настройки установки](https://omniflow-vpn.mintlify.site/ru/operations/installation-settings.md).

## Секреты

Эти значения являются учётными данными, и обращаться с ними следует соответственно:
`APP_DATABASE_URL` (содержит пароль), `APP_REMNAWAVE_TOKEN`, `APP_OPERATOR_TOKEN`,
`APP_TELEGRAM_TOKEN`, `APP_TELEGRAM_WEBHOOK_SECRET`, `APP_CRYPTOBOT_TOKEN`, `APP_YOOKASSA_SECRET` и
два AES-ключа `APP_DATA_ENCRYPTION_KEY` и `APP_BACKUP_ENCRYPTION_KEY`. Никогда не коммитьте `.env`
или боевые учётные данные; репозиторий гоняет в CI сканер секретов именно ради этого.

Механизм вычистки логов Omniflow вырезает токены ботов, bearer- и basic-учётные данные, похожие на карточные
последовательности цифр, идентификаторы устройств и любые URL из всего, что может попасть в строку
лога, в уведомление оператору или в диагностический пакет, — на заявленном размене: чрезмерно
вычищенная строка лога это мелкое неудобство, а утёкшая ссылка на подписку или токен бота это
инцидент безопасности. Это защищает от случайностей, а не от конфигурации: токен, вставленный не в
ту переменную, остаётся токеном, который оператор может прочитать из окружения процесса. Используйте
TLS для внешних подключений к PostgreSQL, Valkey, Remnawave и платёжным провайдерам. Вся картина
описана в [Безопасности](https://omniflow-vpn.mintlify.site/ru/operations/security.md).


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