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

# Разработка

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

Omniflow — это один репозиторий, в котором лежат один модуль Go и одно рабочее пространство Bun.
Модуль Go собирает из одного и того же дерева исходников три бинарных файла — `cmd/api`, `cmd/bot`,
`cmd/worker`, — а рабочее пространство Bun собирает одно приложение Next.js, обслуживающее и личный
кабинет клиента, и панель оператора. Отдельного репозитория для фронтенда нет, отдельного репозитория
для контракта нет, оркестратора сборки нет: вся система — это `make`, `go`, `bun`, `atlas`
и `docker compose`.

Работу в репозитории определяют три свойства его устройства. Сгенерированный код коммитится, поэтому
забытый генератор превращается в diff, который увидит CI. Контракт API в `api/openapi.yaml` —
единственный источник и для серверных типов Go, и для клиента на TypeScript, поэтому изменение
протокола — это правка одного файла и следом два генератора. А миграции базы данных, её
сгенерированный снимок схемы и слой запросов — три отдельных артефакта, которые обязаны двигаться вместе; у этого
процесса есть своя страница — [Изменения базы данных](https://omniflow-vpn.mintlify.site/ru/contributing/database.md).

Канонические инженерные правила лежат в `AGENTS.md` в корне репозитория. Эта страница — о том, как
их выполнять.

## Что должно быть установлено

| Инструмент | Версия | Зачем |
| - | - | - |
| Go | 1.26.10 | `go.mod` требует 1.26.10; CI и `build/go.Dockerfile` фиксируют 1.26.10 |
| Bun | 1.3.14 | Объявлен как `packageManager` в `package.json`; используется для установки зависимостей и всех скриптов рабочего пространства |
| Docker с Compose v2 | — | Поднимает PostgreSQL 18, Valkey 9, шаг миграции и три сервиса на Go |
| Atlas CLI | 1.3.0, чтобы совпадать с образом в compose | Нужен только для создания или применения миграций вне compose |
| Node | 26 или новее | Только чтобы запустить собранный сервер Next.js вне контейнера; скрипты выполняет Bun |

Ещё вам нужна доступная установка Remnawave 3.2.2 и её токен API. API, бот и воркер отказываются
запускать свои коммерческие поверхности без `APP_REMNAWAVE_URL` и `APP_REMNAWAVE_TOKEN`, потому что
Remnawave авторитетна для всего, что выдаёт Omniflow, и офлайн-замены ей не существует. Где проходит
эта граница, описано на странице [Владение данными](https://omniflow-vpn.mintlify.site/ru/architecture/data-ownership.md).

Собственные инструменты Go зафиксированы внутри `go.mod` директивой `tool` — `oapi-codegen`, `sqlc`
и `govulncheck`, — поэтому вы вызываете их как `go tool <name>` и никогда не устанавливаете
отдельно. Их версии двигаются вместе с модулем, и именно это не даёт двум участникам сгенерировать
разный код из одного и того же контракта.

## Первый запуск

**1. Клонируйте и установите зависимости**

```bash theme={null}
git clone https://github.com/kewldan/omniflow.git
cd omniflow
go mod download
bun install
```

`bun install` использует изолированный линкер и учитывает `bunfig.toml`, где задано
`minimumReleaseAge = 86400`: пакет, опубликованный за последние двадцать четыре часа, не
устанавливается, что убирает окно, в котором скомпрометированный релиз оказывается самым свежим
и потому с наибольшей вероятностью будет подхвачен.

**2. Создайте файл окружения**

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

`.env` никогда не коммитится. Заполните как минимум URL и токен Remnawave; добавляйте
`APP_TELEGRAM_TOKEN`, только если собираетесь прогонять сценарии бота. Каждая переменная,
которой владеет Omniflow, имеет префикс `APP_`, а полный список со значениями по умолчанию — в
разделе [Конфигурация](https://omniflow-vpn.mintlify.site/ru/getting-started/configuration.md).

**3. Поднимите бэкенд-стек**

```bash theme={null}
make dev
```

Это `docker compose up --build postgres valkey api bot worker`. Сервис `migrate` не назван в
командной строке, но запускается всё равно, потому что все три сервиса на Go объявляют
`depends_on: migrate: service_completed_successfully` — ничто не обслуживает трафик поверх базы
данных, не мигрированной до head.

**4. Проверьте, что всё поднялось**

```bash theme={null}
curl http://localhost:8080/healthz
```

Здоровый API отвечает `{"status":"ok","version":"dev"}`. Строка версии всегда `dev`: сборка
контейнера не передаёт `-ldflags="-X main.version=…"`, поэтому номер релиза в бинарный файл
ничем не проставляется.

**5. Добавьте веб-приложение**

```bash theme={null}
make up
```

Это `docker compose --profile web up --build`, что добавляет контейнер Next.js на порт 3000.
Личный кабинет клиента находится по адресу `/`, панель оператора — по `/admin`. При первом
запуске API пишет одноразовый токен настройки в свой лог; активируйте его на `/admin/setup`,
чтобы создать первую учётную запись владельца, как описано на странице
[Панель оператора](https://omniflow-vpn.mintlify.site/ru/operations/admin-panel.md).

`make down` останавливает всё, включая профиль web.

> [!NOTE]
>
> Собственные таблицы River поставляются вместе с закоммиченными миграциями Atlas
> (`20260905130000_river_queue.sql`), поэтому сервис `migrate` в compose оставляет базу, на которой
> воркер может запуститься. `cmd/worker/main.go` завершается со статусом 1, когда `river.Client.Start`
> возвращает ошибку, поэтому воркер, направленный на немигрированную базу данных, не остаётся
> работать. См. [Задачи и очереди](https://omniflow-vpn.mintlify.site/ru/operations/jobs-and-queues.md#схема-очереди-поставляется-вместе-с-миграциями).

## Работа над фронтендом вне compose

```bash theme={null}
bun run dev:web
```

Это запускает `next dev` для `@omniflow/web` на порту 3000 против того API, на который вы его
направите. `API_INTERNAL_URL` — с откатом на `NEXT_PUBLIC_API_URL`, а затем на пустую строку — это
адрес того API, и теперь у него две задачи, а не одна.

Первая — собственная проверка `/v1/panel/auth/session`, которую делает *сервер* Next.js. Вторая —
браузерная: сгенерированный клиент держит базовый URL на уровне модуля равным `""`
(`packages/api-client/src/fetcher.ts`), и `setBaseUrl` никто не вызывает, поэтому каждый вызов из
браузера идёт на тот же origin — так задумано, — а `apps/web/proxy.ts` перенаправляет `/v1` на
`API_INTERNAL_URL`, чтобы этим вызовам было куда попасть. Без этого они приходят в Next.js, где
маршрута `/v1` нет, и панель отрисовывается безупречно, проваливая при этом каждый свой запрос.

В продакшене обратный прокси отвечает на `/v1` на веб-хосте раньше, чем запрос доходит до Next.js, и
`proxy.ts` не участвует; см. [Обратный прокси](https://omniflow-vpn.mintlify.site/ru/operations/reverse-proxy.md). Оба пути существуют
потому, что правило одного origin должно выполняться в любой топологии, а такие вещи незаметны, пока
кто-нибудь не попробует войти. `apps/web/e2e/api-reachability.spec.ts` — тот тест, который за этим
следит.

## Структура репозитория

| Путь | Что содержит |
| - | - |
| `cmd/api`, `cmd/bot`, `cmd/worker` | Три точки входа процессов; см. [Обзор архитектуры](https://omniflow-vpn.mintlify.site/ru/architecture/overview.md) |
| `internal/` | Доменные пакеты, транспорт, адаптеры и сгенерированный `database/dbgen` |
| `api/openapi.yaml` | Контракт REST, OpenAPI 3.0.3 |
| `database/` | `migrations/`, сгенерированный снимок `schema.sql` и `queries/` для sqlc |
| `apps/web` | Приложение Next.js, обслуживающее обе панели |
| `packages/api-client` | Сгенерированные привязки Fetch, SWR и Zod |
| `packages/ui` | Общие примитивы shadcn, используемые обеими поверхностями. Никаких нативных `<select>`, полей даты и времени: используйте `Select`, комбобокс на `Command` или `DateTimeField` |
| `docs/` | Эта документация; см. [Документация](https://omniflow-vpn.mintlify.site/ru/contributing/documentation.md) |
| `deploy/proxies` | Примеры Caddy и Traefik, проверяемые в CI |

`AGENTS.md` ограничивает, что и откуда может импортироваться: доменные пакеты не должны
импортировать транспорт, генерацию базы данных, Telegram или пакеты, специфичные для провайдеров, а
внешние системы находятся за небольшими интерфейсами. Он же фиксирует три префикса API — `/v1/panel`
для панели оператора, `/v1/admin` для поверхности с bearer-токеном, `/v1/account` для клиентского
кабинета — и запрещает их объединять, потому что иначе каждый унаследовал бы middleware другого и
клиентские учётные данные смогли бы дотянуться до операторского маршрута.

## Генераторы

Коммитятся четыре вида сгенерированных артефактов. Вы запускаете генератор, когда меняется его вход,
а CI перезапускает их все и падает на любом diff. Этот заслон и есть причина, по которой репозиторий
может доверять сгенерированному файлу, который он не производил только что.

| Меняете это | Запускаете это | Перегенерируется |
| - | - | - |
| `api/openapi.yaml` | `go tool oapi-codegen -config api/oapi-codegen.yaml api/openapi.yaml` | `internal/httpapi/apigen/openapi.gen.go` |
| `api/openapi.yaml` | `bun run generate:api` | `packages/api-client/src/generated/omniflow.ts` и `schemas.ts` |
| `database/queries/*.sql` или миграция | `go tool sqlc generate` | `internal/database/dbgen` |
| Миграция | `sh tools/schema-snapshot.sh` | `database/schema.sql` |

`make generate` запускает первые три по порядку. Шаг Atlas намеренно в него не входит: миграция —
это отрецензированный артефакт, а не результат сборки, и она разобрана на странице
[Изменения базы данных](https://omniflow-vpn.mintlify.site/ru/contributing/database.md).

### Что производит каждый из них

**oapi-codegen** выдаёт пакет `apigen` с моделями, сервером на chi и обёрткой strict-server
(`api/oapi-codegen.yaml`). `skip-prune` выставлен в false, поэтому схема, на которую больше не
ссылается ни одна операция, исчезает из сгенерированного файла, а не остаётся в нём.

**Orval** запускается из `orval.config.ts` с `--formatter biome --fail-on-warnings`, поэтому
предупреждение контракта роняет команду, а не попадает в вывод. Он выпускает две цели: единый клиент
SWR плюс Fetch и единый модуль схем Zod. У клиентской цели выставлено `clean: true`, то есть
сгенерированный каталог сначала очищается — никогда не правьте руками ничего внутри
`packages/api-client/src/generated`, потому что следующий запуск это удалит.

**sqlc** читает `sqlc.yaml`, и его `schema:` — это `database/migrations`, а не
`database/schema.sql`. Поэтому sqlc проверяет типы ваших запросов против истории миграций ровно так,
как её проигрывала бы установка, и именно это превращает запрос, ссылающийся на колонку, которую не
добавляет ни одна миграция, в ошибку генерации, а не времени выполнения. Вывод —
`internal/database/dbgen` на `pgx/v5`, с тегами JSON, интерфейсом `Querier` и пустыми слайсами
вместо nil.

> [!NOTE]
>
> Писать тип протокола руками вместо того, чтобы его сгенерировать, прямо запрещено в `AGENTS.md`:
> контракт — единственное определение, а продублированный тип — это определение, которое молча
> расходится. По этой же причине за `bun ci` и `bun run generate:api` в CI следует
> `git diff --exit-code`.

## Форматирование и линтинг

Go форматируется `gofmt` и больше ничем. CI выполняет `gofmt -w . && git diff --exit-code`, поэтому
неотформатированный код падает как diff, а не как сообщение линтера.

TypeScript, JavaScript и CSS полностью обслуживает Biome 2.5.7 — это единственный форматтер и
единственный линтер, и `AGENTS.md` запрещает добавлять второй. `biome.json` задаёт отступ в два
пробела, ширину строки 100, двойные кавычки и обязательные точки с запятой, а также исключает
`.next`, `node_modules`, `dist`, `*.gen.*`, `test-results` и `playwright-report`.

```bash theme={null}
bun run format   # biome format --write .
bun run lint     # biome lint .
bun run check    # per workspace: tsc --noEmit, then biome check .
make web-check   # the same as bun run check
```

`bun run check` разворачивается по рабочим пространствам `@omniflow/*`. `@omniflow/web` и
`@omniflow/ui` выполняют `tsc --noEmit && biome check .`; `@omniflow/api-client` только проверяет
типы, потому что его содержимое сгенерировано и отформатировано проходом Biome внутри Orval.

Bun — единственный пакетный менеджер. `bun.lock` коммитится как текстовый lock-файл, а lock-файлы
npm, pnpm и Yarn запрещены: один резолвер означает один граф зависимостей, а второй lock-файл — это
второй ответ на вопрос, какая версия установлена.

## Тесты

```bash theme={null}
make go-test    # go test -race -cover ./...
```

Стандартный набор тестов Go выполняется без демона Docker и без сети, и именно это свойство
обеспечивает, что его действительно запускают. Всё, что требует инфраструктуры, живёт за build-тегом
`integration`:

```bash theme={null}
go test -tags=integration -race -timeout=20m ./internal/integrationtest/...
```

Этот набор поднимает PostgreSQL через Testcontainers и применяет закоммиченные файлы миграций по
порядку, поэтому он является заслоном не только для репозиториев, но и для миграций. Браузерное
покрытие лежит в `apps/web/e2e` и выполняется против продакшен-сборки:

```bash theme={null}
bun run build
bun --filter @omniflow/web e2e:install
bun --filter @omniflow/web e2e
```

Конфигурация Playwright сама запускает `bun run start`, если не задан `PLAYWRIGHT_BASE_URL`. Что
покрывает каждый набор и что обязано принести в тестах изменение платежей, кошелька,
аутентификации, RBAC, импорта или выдачи доступа, описано на странице
[Стратегия тестирования](https://omniflow-vpn.mintlify.site/ru/contributing/testing.md).

Проверки безопасности собраны в одну цель:

```bash theme={null}
make security   # go tool govulncheck ./... and gitleaks detect --source .
```

## Что обеспечивает CI

`.github/workflows/ci.yml` запускается на каждый pull request и каждый push в `main`, причём
выполняющиеся запуски отменяются в пределах одного ref.

| Задача | Проверка |
| - | - |
| Go | Перегенерирует вывод oapi-codegen и sqlc, затем `gofmt -w . && git diff --exit-code`, `go vet ./...`, `go test -race -coverprofile=coverage.out ./...`, `go tool govulncheck ./...` |
| Web | `bun ci`, `bun run generate:api`, `git diff --exit-code`, `bun run check`, `bun run build` |
| Сквозные тесты (Playwright) | Собирает, устанавливает Chromium и WebKit, прогоняет набор; при падении выкладывает отчёт как артефакт |
| Интеграционные тесты (Testcontainers) | `go test -tags=integration -race -timeout=20m ./internal/integrationtest/...` |
| Миграции Atlas | `atlas migrate validate`, затем для каждой базовой точки: применить базовую точку, обновиться до head, применить head ещё раз, чтобы доказать, что это no-op |
| Compose и обратные прокси | Копирует `.env.example` в `.env`, проверяет базовый файл compose и оверлей Traefik, проверяет Caddyfile и собирает образы `api`, `bot` и `worker` |
| Документация Mintlify | `mint validate` и `mint broken-links --check-anchors` |
| Заголовок PR в стиле Conventional | Заголовок pull request должен быть Conventional Commit |

Еженедельный workflow безопасности добавляет gitleaks, сканирование файловой системы Trivy с
отправкой находок уровня CRITICAL и HIGH в code scanning, а также dependency review.

> [!NOTE]
>
> Задача CI может зависеть только от закоммиченных файлов. Всё, что вы создаёте локально — прежде
> всего `.env`, — должно создаваться шагом внутри самой задачи, поэтому задача compose копирует
> `.env.example`, а не рассчитывает на секрет. Задача, читающая незакоммиченный файл, делает
> конвейер зелёным на вашей машине и больше нигде.

## Коммиты и pull request'ы

Используйте [Conventional Commits](https://www.conventionalcommits.org/). Допустимые типы — `feat`,
`fix`, `docs`, `refactor`, `test`, `build`, `ci`, `perf`, `chore` и `revert`, а область обычно
указывает участок: `feat(bot):`, `fix(billing):`, `docs(telemetry):`. Release Please читает историю,
чтобы собрать `CHANGELOG.md` и предложить версию, поэтому опечатка в типе — это потерянная запись в
списке изменений. Никогда не добавляйте в коммит или pull request отметку о генерации или упоминание
ассистента; это правило перекрывает любое соглашение инструмента, которое такую отметку дописывает.

Два правила из `AGENTS.md` влияют на ревью сильнее любого стилевого пункта:

* **Доводите возможность до конца в рамках одного изменения.** Таблица, запрос или обработчик, до
  которых ничто не дотягивается, — это не задел, а незавершённая работа.
* **Сообщайте, что действительно выполнялось.** Проверка, которую вы не смогли запустить локально, —
  не пройденная, а непроверенная: назовите её и назовите причину. «Сборка и юнит-тесты проходят»
  никогда не является утверждением, что были задействованы миграции, контейнеры или детектор гонок.

Авторизация вкомпилирована и живёт только в `internal/rbac`. Никогда не заводите второе место,
решающее, что позволено роли, и никогда не позволяйте скрытому маршруту или состоянию на фронтенде
быть единственным, что обеспечивает право доступа.

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

> **[Изменения базы данных](https://omniflow-vpn.mintlify.site/ru/contributing/database.md)**
>
> Желаемая схема, миграции Atlas, `atlas.sum` и таблицы, доступные только на добавление.

> **[Стратегия тестирования](https://omniflow-vpn.mintlify.site/ru/contributing/testing.md)**
>
> Что выполняется по умолчанию, чему нужен Docker и что обязано принести финансовое изменение.

> **[Документация](https://omniflow-vpn.mintlify.site/ru/contributing/documentation.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.