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
Проверьте, что всё поднялось
{"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-файл — это
второй ответ на вопрос, какая версия установлена.
Тесты
integration:
apps/web/e2e и выполняется против продакшен-сборки:
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 в продакшене.