> ## 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.

# Быстрый старт

> Поднимите стек Compose, создайте первого владельца, включите двухфакторную аутентификацию и убедитесь, что установка здорова

Эта страница доводит клон репозитория до работающей установки с учётной записью оператора, под
которой можно войти. Она пока ничего не продаёт — это [Ваша первая продажа](https://omniflow-vpn.mintlify.site/ru/getting-started/first-sale.md),
которая начинается там, где заканчивается эта страница.

Всё описанное ниже — локальный стек Compose, поставляемый в репозитории. Он публикует порты на
`localhost`, использует учётные данные из `compose.yaml` и не терминирует TLS. Для сервера, смотрящего
в интернет, прочитайте [Развёртывание](https://omniflow-vpn.mintlify.site/ru/operations/deployment.md) и
[Обратный прокси](https://omniflow-vpn.mintlify.site/ru/operations/reverse-proxy.md), прежде чем что-либо выставлять наружу.

## Перед началом

| Что нужно | Зачем |
| - | - |
| Docker с Compose v2 | Весь стек, включая запуск миграций, состоит из сервисов Compose |
| Доступная установка Remnawave 3.2.2 и токен API | `api`, `bot` и `worker` все отказываются стартовать без `APP_REMNAWAVE_URL` и `APP_REMNAWAVE_TOKEN`, и именно этот токен использует проба готовности |
| `openssl` или любой другой способ получить случайные байты | Два секрета нужно сгенерировать до запуска стека |
| Токен Telegram-бота от [@BotFather](https://t.me/BotFather) | Здесь необязателен. С пустым `APP_TELEGRAM_TOKEN` процесс бота запускается, пишет `telegram bot disabled because APP_TELEGRAM_TOKEN is empty` и простаивает — это не ошибка |

## Что делает каждый сервис

`compose.yaml` описывает семь сервисов. Порт на хост публикуют только `api` и `web`; PostgreSQL и
Valkey остаются во внутренней сети Compose.

| Сервис | Образ или сборка | Что делает |
| - | - | - |
| `postgres` | `postgres:18.4-alpine` | Единственный источник истины. Том `postgres-data`, проверка здоровья через `pg_isready` |
| `valkey` | `valkey/valkey:9.1.1-alpine` | Ограничения частоты запросов, защита от повторов, блокировки и временное состояние. Запускается с `--save "" --appendonly no`, потому что ничего долговечного здесь не хранится |
| `migrate` | `arigaio/atlas:1.3.0` | Применяет закоммиченные миграции Atlas и завершается. Каждый прикладной сервис ждёт его успешного завершения |
| `api` | `build/go.Dockerfile`, `TARGET=api` | HTTP API на `:8080`: `/v1/catalog`, `/v1/payments/webhooks/{provider}`, `/v1/admin`, `/v1/panel`, `/v1/account`, пробы, `/metrics`, обнаружение режима обслуживания и токен первичной настройки |
| `bot` | `TARGET=bot`, `RUNTIME=postgres` | Обновления Telegram, уведомления операторам, нотификатор клиентов и поверхность резервного копирования `/ops`. Несёт клиентские утилиты PostgreSQL, потому что может сделать резервную копию по требованию |
| `worker` | `TARGET=worker`, `RUNTIME=postgres` | Долговечные задания, обслуживание заказов и корзин, сверщик платежей, сверка исполнения заказов, продления и работа с задолженностью, свипы, разворачивание кампаний, хранение данных и резервные копии по расписанию |
| `web` | `build/web.Dockerfile`, профиль `web` | Приложение Next.js на `:3000`, обслуживающее и `/account`, и `/admin` |

> [!NOTE]
>
> У сервиса `migrate` DSN для разработки
> `postgres://omniflow:omniflow@postgres:5432/omniflow?sslmode=disable` жёстко зашит в команду, и
> `.env` он не читает. Если вы меняете учётные данные базы данных, меняйте их и в `compose.yaml`,
> иначе миграции будут применены не туда, куда смотрит приложение.

## Настройте установку

Вся конфигурация, принадлежащая проекту, — это переменные окружения с префиксом `APP_`, читаемые
один раз при старте. Конфигурационного файла нет, горячей перезагрузки нет: изменение значения
означает перезапуск процесса, который его читает.

**1. Скопируйте файл-пример**

```bash theme={null}
cp .env.example .env
```

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

**2. Сгенерируйте два секрета**

```bash theme={null}
openssl rand -base64 32   # APP_DATA_ENCRYPTION_KEY
openssl rand -hex 32      # APP_OPERATOR_TOKEN
```

`APP_DATA_ENCRYPTION_KEY` должен декодироваться ровно в 32 байта, иначе запуск падает с
`APP_DATA_ENCRYPTION_KEY must be base64-encoded 32 bytes`. Им запечатываются контактные данные,
TOTP-секреты, клиентские секреты OIDC, учётные данные провайдеров и короткоживущая cookie
процесса входа — потеря ключа означает потерю доступа ко всему этому.

`APP_OPERATOR_TOKEN` — предъявительские учётные данные для всего префикса `/v1/admin`,
сравниваемые за постоянное время. Подойдёт любая случайная строка с высокой энтропией.

**3. Заполните значения, которые нужны процессам**

```env theme={null}
APP_DATABASE_URL=postgres://omniflow:omniflow@postgres:5432/omniflow?sslmode=disable
APP_VALKEY_URL=redis://valkey:6379/0
APP_REMNAWAVE_URL=https://panel.example.com
APP_REMNAWAVE_TOKEN=<your Remnawave API token>
APP_OPERATOR_TOKEN=<generated above>
APP_DATA_ENCRYPTION_KEY=<generated above>
APP_DEFAULT_CURRENCY=RUB
APP_TELEGRAM_TOKEN=<from BotFather, or leave empty>
```

Отказы намеренны и происходят при старте, а не на первом запросе:

| Процесс | Отказывается стартовать без |
| - | - |
| `api` | Без ничего, если `APP_DATABASE_URL` пуст, — но тогда он обслуживает только пробы и `/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_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` и выполняет всё остальное |

Другие значения валидируются так же: `APP_DEFAULT_CURRENCY` после приведения к верхнему регистру
должен совпадать с `^[A-Z]{3}$`, `APP_TELEGRAM_WEBHOOK_URL` должен быть HTTPS, а
`APP_TELEMETRY_ENDPOINT` должен быть HTTPS-URL, пока телеметрия включена.

**4. Разрешите cookie по обычному HTTP для локального стека**

```env theme={null}
APP_ADMIN_COOKIE_SECURE=false
APP_CUSTOMER_COOKIE_SECURE=false
```

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

## Поднимите стек

```bash theme={null}
docker compose up --build postgres valkey migrate api bot worker
```

`migrate` запускается первым и завершается; `api`, `bot` и `worker` стартуют после его успешного
завершения и после того, как Valkey отчитается о здоровье. Затем запустите веб-приложение, которое
обслуживает и клиентский кабинет, и операторское рабочее место:

```bash theme={null}
docker compose --profile web up --build
```

После этого:

* API — `http://localhost:8080`
* Клиентский кабинет — `http://localhost:3000`
* Рабочее место оператора — `http://localhost:3000/admin`

`make dev` — это первая команда без `migrate` в списке аргументов, `make up` — всё вместе с профилем
`web`, а `make down` останавливает всё. Полный перечень целей выдаёт `make help`.

## Создайте первого владельца

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

**1. Прочитайте токен из лога API**

```bash theme={null}
docker compose logs api | grep setup
```

```
level=WARN msg="no administrator exists yet; redeem this one-time setup token at /admin/setup" token=… expires_in=30m0s
```

Токен действует 30 минут и может быть использован один раз. Перезапуск API, пока владельца нет,
выпускает новый; как только владелец есть, перезапуск не печатает ничего, а
`POST /v1/panel/bootstrap` навсегда отвечает `409 bootstrap_closed`.

**2. Используйте его на /admin/setup**

Откройте `http://localhost:3000/admin/setup`. Форма запрашивает токен настройки, отображаемое
имя длиной 1–80 символов, адрес электронной почты и пароль длиной 12–256 символов. Требований к
составу пароля нет; единственное требование — длина, и форма в браузере в точности повторяет то,
что проверяет API.

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

Отказ конкретен: `422 weak_password` для слишком короткого пароля, `409 email_in_use` для уже
зарегистрированного адреса, `409 bootstrap_closed` после того, как первичная настройка завершена.

**3. Войдите**

Вас перенаправляет на `/admin/login`. Введите адрес и пароль. У учётной записи ещё нет второго
фактора, поэтому вы попадаете на дашборд по адресу `/admin`.

Вход отвечает одинаково на неверный пароль, неизвестный адрес и отключённую учётную запись —
все три случая дают `401 invalid_credentials`, — поэтому эндпоинт нельзя использовать, чтобы
выяснить, за какими адресами закреплены учётные записи операторов. Частота ограничена десятью
попытками в минуту на адрес и тридцатью в минуту на IP клиента, а если Valkey недоступен,
проверка отказывает в доступе.

## Включите двухфакторную аутентификацию

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

**1. Начните настройку**

Откройте `/admin/security` и нажмите **Настроить двухфакторную**. В ответе приходят TOTP-секрет
и URI `otpauth://`. Секрет показывается и текстом, и в QR-коде, потому что аутентификатор,
работающий на том же устройстве, не может отсканировать экран, на котором он сам находится.

Подпись, которую покажет ваш аутентификатор, берётся из `APP_ADMIN_TOTP_ISSUER`, по умолчанию
равного `Omniflow`.

**2. Подтвердите первый код**

Введите шестизначный код и отправьте его. Пока код не подтверждён, секрет хранится
неподтверждённым и не удовлетворяет ни одной проверке при входе, поэтому недоделанная настройка
никогда не может заблокировать вам доступ. Неверный код отвечает `422 invalid_code`;
подтверждение без начала настройки отвечает `409 totp_not_started`.

**3. Сохраните резервные коды**

Подтверждение возвращает десять одноразовых резервных кодов. Они показываются один раз и больше
никогда — перевыпуск заменяет весь набор и обесценивает каждый неиспользованный код. Поле кода
при входе принимает 6–24 символа и является текстовым, а не числовым, потому что резервный код
содержит буквы.

Отключение второго фактора требует пароля от учётной записи, потому что снятие фактора — это ровно
то, что попробует сделать атакующий с угнанной сессией. Смена пароля завершает все остальные сессии,
а **Завершить все остальные** намеренно щадит ту сессию, из которой вы защищаете учётную запись.

Сессиями управляют три числа: 30-минутный таймаут бездействия, который сдвигается, 12-часовой
абсолютный таймаут, который не сдвигается никогда, и ротация токена каждые 15 минут.

## Убедитесь, что всё работает

Живость и готовность отвечают на разные вопросы и намеренно вынесены в отдельные эндпоинты: процесс,
который работает, но не может достучаться до PostgreSQL, следует убрать из балансировщика, а не
перезапускать. `/healthz` — тот же обработчик, что и `/livez`, оставленный для развёртываний, которые
уже его опрашивают.

```bash theme={null}
curl -s http://localhost:8080/livez
# {"status":"ok","version":"dev"}

curl -s http://localhost:8080/readyz
```

Здоровый API отвечает `200`:

```json theme={null}
{
  "status": "ready",
  "checks": [
    {"name": "postgres", "healthy": true, "checkedAt": "2026-08-13T09:12:04Z", "latencyMs": 2},
    {"name": "valkey", "healthy": true, "checkedAt": "2026-08-13T09:12:04Z", "latencyMs": 1},
    {"name": "remnawave", "healthy": true, "checkedAt": "2026-08-13T09:12:04Z", "latencyMs": 84}
  ]
}
```

Нездоровая зависимость превращает ответ в `503` с `"status": "degraded"`, а отказавшая проверка несёт
`"error": "timeout"` или `"error": "unavailable"` плюс счётчик `consecutiveFailures`. Эти два слова —
единственные существующие значения ошибки: ошибка pgx или HTTP регулярно содержит DSN, имя хоста или
учётные данные, поэтому проба сообщает классификацию вместо неё. Проба `remnawave` — это
аутентифицированный вызов, а значит, отозванный токен API проявится здесь как нездоровая
зависимость, а не как загадочный сбой выдачи доступа позже.

Какие пробы регистрирует каждый процесс, различается, и это важно:

| Процесс | Пробы | Приёмник |
| - | - | - |
| `api` с настроенной базой данных | `postgres`, `valkey`, `remnawave` | `APP_HTTP_ADDR`, по умолчанию `:8080` |
| `api` с пустым `APP_DATABASE_URL` | нет ни одной — `/readyz` отвечает `200` с пустым массивом `checks` | тот же |
| `worker` | `postgres` | `APP_WORKER_HTTP_ADDR`, по умолчанию `:8081`; пустое значение отключает приёмник |
| `bot` | `postgres`, и только когда существует пул | `APP_BOT_METRICS_ADDR`, **по умолчанию пуст**, поэтому из коробки у бота нет никакого HTTP-эндпоинта |

> [!WARNING]
>
> API, запущенный без `APP_DATABASE_URL`, сообщает о своей готовности, обслуживая почти ничего. Если
> `/readyz` зелёный, а `/v1/catalog/plans` не существует, проверьте, дошёл ли URL базы данных до
> контейнера.

Под учётной записью оператора та же информация доступна на `/admin` в разделе **Система → Состояние**
(`GET /v1/panel/overview/health`, право доступа `system.read`) — вместе со здоровьем платёжных
провайдеров и провайдеров цифровых товаров, которое HTTP-проба не покрывает.

## Что не работает на свежем клоне

Это текущие факты о `main`, а не ошибки конфигурации. Прочитайте их, прежде чем заключить, что ваша
установка сломана.

* **У долговечной очереди заданий нет схемы.** Omniflow ставит фоновую работу в очередь через River,
  но ни одна закоммиченная миграция не создаёт собственных таблиц River, и ничто в стеке не
  запускает мигратор River. Поэтому на базе, мигрированной только через `docker compose up migrate`,
  `worker` завершается при старте с записью `start River`, а любой расчёт — который вставляет
  задание исполнения в той же транзакции, что и деньги, — откатывается. Покупка не может
  завершиться, пока не появится эта миграция. О том, где именно останавливается тестовая покупка,
  см. [Ваша первая продажа](https://omniflow-vpn.mintlify.site/ru/getting-started/first-sale.md), а о том, что очередь должна делать, —
  [Задания и очереди](https://omniflow-vpn.mintlify.site/ru/operations/jobs-and-queues.md).
* **Транзакционный outbox пишется и никогда не публикуется.** Вставляются три темы, `published_at`
  никто никогда не проставляет, и диспетчера не существует. Поэтому счётчики outbox на дашборде и в
  `GET /v1/admin/outbox` растут навсегда, `APP_RETENTION_OUTBOX_DAYS` не удаляет ничего, а пункт
  внимания «отставание outbox» на `/admin` ожидаем, а не диагностичен.
* **Провайдера трассировки нет.** Код открывает спаны в HTTP, обновлениях Telegram, заданиях и SQL,
  но ни один экспортёр не установлен и ни одна переменная `OTEL_*` нигде в репозитории не читается.
  Установка стандартных переменных OpenTelemetry ничего не меняет; глобальный провайдер остаётся
  пустышкой. То, что установка на самом деле отдаёт, — это метрики и логи, см.
  [Наблюдаемость](https://omniflow-vpn.mintlify.site/ru/operations/observability.md).
* **Каждый бинарник сообщает `"version":"dev"`.** Версия не проставляется при сборке нигде, включая
  выпущенные образы, поэтому `/livez`, диагностический пакет и heartbeat телеметрии одинаково
  говорят `dev`.
* **Разделы настроек в панели не конфигурируют рантайм.** Сохранение раздела складывает документ;
  подключение к Remnawave, бот, группа операторов, политика обслуживания, телеметрия, резервные
  копии и платёжные адаптеры берутся из переменных окружения, прочитанных при старте. См.
  [Настройки установки](https://omniflow-vpn.mintlify.site/ru/operations/installation-settings.md).
* **Упавший процесс никто не перезапускает.** Ни один основной сервис Compose не объявляет политику
  `restart:`, поэтому завершившийся процесс остаётся лежать, пока кто-нибудь не заметит. Добавьте её
  всему, что должно работать долго.
* **У вложения к обращению, загруженного из браузера, нет тома.** `APP_SUPPORT_ATTACHMENT_DIR` по
  умолчанию равен `/var/lib/omniflow/support-attachments` и читается и `api`, и `worker`, но
  `compose.yaml` не монтирует туда том ни для одного из них. Файлы живут в записываемом слое
  контейнера, пока его не пересоберут, а воркер не может освободить место от файлов, которых не
  видит.

## Дальнейшие шаги

> **[Ваша первая продажа](https://omniflow-vpn.mintlify.site/ru/getting-started/first-sale.md)**
>
> Создайте тариф, назначьте цену версии, настройте провайдера, подключите бота и проведите тестовую
> покупку.

> **[Справочник по конфигурации](https://omniflow-vpn.mintlify.site/ru/getting-started/configuration.md)**
>
> Каждая переменная, её значение по умолчанию, какой процесс её читает и что она меняет.

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

> **[Обратный прокси](https://omniflow-vpn.mintlify.site/ru/operations/reverse-proxy.md)**
>
> TLS, ограничения на размер тела и то, как держать `/metrics` и `/readyz` вне публичного хоста.

> **[Резервное копирование и восстановление](https://omniflow-vpn.mintlify.site/ru/operations/backup-restore.md)**
>
> Включите шифрованные резервные копии до того, как появится что терять.

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


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