Skip to main content
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, поэтому изменение протокола — это правка одного файла и следом два генератора. А миграции базы данных, её сгенерированный снимок схемы и слой запросов — три отдельных артефакта, которые обязаны двигаться вместе; у этого процесса есть своя страница — Изменения базы данных. Канонические инженерные правила лежат в AGENTS.md в корне репозитория. Эта страница — о том, как их выполнять.

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

Ещё вам нужна доступная установка Remnawave 3.2.2 и её токен API. API, бот и воркер отказываются запускать свои коммерческие поверхности без APP_REMNAWAVE_URL и APP_REMNAWAVE_TOKEN, потому что Remnawave авторитетна для всего, что выдаёт Omniflow, и офлайн-замены ей не существует. Где проходит эта граница, описано на странице Владение данными. Собственные инструменты Go зафиксированы внутри go.mod директивой tool — oapi-codegen, sqlc и govulncheck, — поэтому вы вызываете их как go tool <name> и никогда не устанавливаете отдельно. Их версии двигаются вместе с модулем, и именно это не даёт двум участникам сгенерировать разный код из одного и того же контракта.

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

1

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

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

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

.env никогда не коммитится. Заполните как минимум URL и токен Remnawave; добавляйте APP_TELEGRAM_TOKEN, только если собираетесь прогонять сценарии бота. Каждая переменная, которой владеет Omniflow, имеет префикс APP_, а полный список со значениями по умолчанию — в разделе Конфигурация.
3

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

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

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

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

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

Это docker compose --profile web up --build, что добавляет контейнер Next.js на порт 3000. Личный кабинет клиента находится по адресу /, панель оператора — по /admin. При первом запуске API пишет одноразовый токен настройки в свой лог; активируйте его на /admin/setup, чтобы создать первую учётную запись владельца, как описано на странице Панель оператора.
make down останавливает всё, включая профиль web.
Собственные таблицы River поставляются вместе с закоммиченными миграциями Atlas (20260905130000_river_queue.sql), поэтому сервис migrate в compose оставляет базу, на которой воркер может запуститься. cmd/worker/main.go завершается со статусом 1, когда river.Client.Start возвращает ошибку, поэтому воркер, направленный на немигрированную базу данных, не остаётся работать. См. Задачи и очереди.

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

Это запускает 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 не участвует; см. Обратный прокси. Оба пути существуют потому, что правило одного origin должно выполняться в любой топологии, а такие вещи незаметны, пока кто-нибудь не попробует войти. apps/web/e2e/api-reachability.spec.ts — тот тест, который за этим следит.

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

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

Генераторы

Коммитятся четыре вида сгенерированных артефактов. Вы запускаете генератор, когда меняется его вход, а CI перезапускает их все и падает на любом diff. Этот заслон и есть причина, по которой репозиторий может доверять сгенерированному файлу, который он не производил только что. make generate запускает первые три по порядку. Шаг Atlas намеренно в него не входит: миграция — это отрецензированный артефакт, а не результат сборки, и она разобрана на странице Изменения базы данных.

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

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.
Писать тип протокола руками вместо того, чтобы его сгенерировать, прямо запрещено в 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.
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-файл — это второй ответ на вопрос, какая версия установлена.

Тесты

Стандартный набор тестов Go выполняется без демона Docker и без сети, и именно это свойство обеспечивает, что его действительно запускают. Всё, что требует инфраструктуры, живёт за build-тегом integration:
Этот набор поднимает PostgreSQL через Testcontainers и применяет закоммиченные файлы миграций по порядку, поэтому он является заслоном не только для репозиториев, но и для миграций. Браузерное покрытие лежит в apps/web/e2e и выполняется против продакшен-сборки:
Конфигурация Playwright сама запускает bun run start, если не задан PLAYWRIGHT_BASE_URL. Что покрывает каждый набор и что обязано принести в тестах изменение платежей, кошелька, аутентификации, RBAC, импорта или выдачи доступа, описано на странице Стратегия тестирования. Проверки безопасности собраны в одну цель:

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

.github/workflows/ci.yml запускается на каждый pull request и каждый push в main, причём выполняющиеся запуски отменяются в пределах одного ref. Еженедельный workflow безопасности добавляет gitleaks, сканирование файловой системы Trivy с отправкой находок уровня CRITICAL и HIGH в code scanning, а также dependency review.
Задача CI может зависеть только от закоммиченных файлов. Всё, что вы создаёте локально — прежде всего .env, — должно создаваться шагом внутри самой задачи, поэтому задача compose копирует .env.example, а не рассчитывает на секрет. Задача, читающая незакоммиченный файл, делает конвейер зелёным на вашей машине и больше нигде.

Коммиты и pull request’ы

Используйте Conventional Commits. Допустимые типы — 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. Никогда не заводите второе место, решающее, что позволено роли, и никогда не позволяйте скрытому маршруту или состоянию на фронтенде быть единственным, что обеспечивает право доступа.

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

Изменения базы данных

Желаемая схема, миграции Atlas, atlas.sum и таблицы, доступные только на добавление.

Стратегия тестирования

Что выполняется по умолчанию, чему нужен Docker и что обязано принести финансовое изменение.

Документация

Как писать, проверять и держать русское дерево в актуальном состоянии.

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

Во что превращается топология compose в продакшене.