cmd/api, cmd/bot, cmd/worker — from the same source tree, and the Bun workspace
builds one Next.js application that serves both the customer account and the operator panel. There
is no separate frontend repository, no separate contract repository, and no build orchestrator: the
whole system is make, go, bun, atlas, and docker compose.
Three things about the repository shape decide how you work in it. Generated code is committed, so a
generator you forget to run is a diff CI can see. The API contract in api/openapi.yaml is the
single source for both the Go server types and the TypeScript client, so a wire change is one file
edit followed by two generators. And the database’s migrations, its generated schema snapshot, and its query
layer are three separate artefacts that have to move together — that workflow has its own page,
Database changes.
The canonical engineering rules live in AGENTS.md at the repository root. This page is how you
carry them out.
What you need installed
You also need a reachable Remnawave 3.2.2 installation and its API token. The API, bot, and worker
all refuse to start their commerce surfaces without
APP_REMNAWAVE_URL and APP_REMNAWAVE_TOKEN,
because Remnawave is authoritative for everything Omniflow provisions and there is no offline
substitute for it. See Data ownership for where that boundary sits.
Go’s own tools are pinned inside go.mod under a tool directive — oapi-codegen, sqlc, and
govulncheck — so you invoke them as go tool <name> and never install them separately. Their
versions move with the module, which is what stops two contributors from generating different code
from the same contract.
First run
1
Clone and install
bun install uses the isolated linker and honours bunfig.toml, which sets
minimumReleaseAge = 86400 — a package published in the last twenty-four hours is not
installed, which removes the window in which a compromised release is newest and therefore
most likely to be picked up.2
Create the environment file
.env is never committed. Fill in at least the Remnawave URL and token; add
APP_TELEGRAM_TOKEN only if you intend to exercise bot flows. Every variable Omniflow owns is
prefixed APP_, and the complete list with defaults is in
Configuration.3
Start the backend stack
docker compose up --build postgres valkey api bot worker. The migrate service is
not named on the command line but starts anyway, because all three Go services declare
depends_on: migrate: service_completed_successfully — nothing serves traffic against a
database that has not been migrated to head.4
Check it came up
{"status":"ok","version":"dev"}. The version string is always dev:
the container build passes no -ldflags="-X main.version=…", so nothing stamps a release
number into the binary.5
Add the web application
docker compose --profile web up --build, which adds the Next.js container on port
3000. The customer account is at / and the operator panel at /admin. On first start the API
writes a one-time setup token to its log; redeem it at /admin/setup to create the first owner
account, as described in Admin panel.make down stops everything, including the web profile.
River’s own tables ship with the committed Atlas migrations (
20260905130000_river_queue.sql), so
the compose migrate service leaves a database the worker can start on. cmd/worker/main.go exits
with status 1 when river.Client.Start returns an error, so a worker pointed at a database that
has not been migrated does not stay up. See Jobs and queues.Working on the frontend outside compose
next dev for @omniflow/web on port 3000 against whatever API you point it at.
API_INTERNAL_URL — falling back to NEXT_PUBLIC_API_URL, then to an empty string — is where that
API lives, and it now does two jobs rather than one.
The first is the Next.js server’s own /v1/panel/auth/session check. The second is the browser’s:
the generated client keeps a module-level base URL of ""
(packages/api-client/src/fetcher.ts) and nothing calls setBaseUrl, so every call from the browser
is same-origin by design, and apps/web/proxy.ts forwards /v1 to API_INTERNAL_URL so that
those calls land somewhere. Without it they reach Next.js, which has no /v1 route, and the panel
renders perfectly while failing every request it makes.
In production the reverse proxy answers /v1 on the web host before Next.js sees it, and the
proxy file is inert; see Reverse proxy. Both paths exist because the
same-origin rule has to hold in every topology, and it is the sort of thing that is invisible until
somebody tries to sign in. apps/web/e2e/api-reachability.spec.ts is the gate that keeps it honest.
Repository layout
AGENTS.md constrains what may import what: domain packages must not import transport, database
generation, Telegram, or provider-specific packages, and external systems sit behind small
interfaces. It also fixes the three API prefixes — /v1/panel for the operator panel, /v1/admin
for the bearer-token surface, /v1/account for the customer panel — and forbids merging them,
because each would otherwise inherit another’s middleware and a customer credential would be able to
reach an operator route.
The generators
Four kinds of generated artefact are committed. You run a generator when its input changes, and CI re-runs all of them and fails on any diff. That gate is the reason the repository can trust a generated file it did not just produce.make generate runs the first three in order. The Atlas step is deliberately not part of it: a
migration is a reviewed artefact, not a build output, and it is covered on
Database changes.
What each one produces
oapi-codegen emits packageapigen with models, a chi server, and the strict-server wrapper
(api/oapi-codegen.yaml). skip-prune is false, so a schema no longer referenced by any operation
disappears from the generated file rather than lingering.
Orval runs from orval.config.ts with --formatter biome --fail-on-warnings, so a contract
warning fails the command instead of being written into the output. It emits two targets: a single
SWR-plus-Fetch client and a single Zod schema module. The client target sets clean: true, which
means the generated directory is emptied first — never hand-edit anything under
packages/api-client/src/generated, because the next run deletes it.
sqlc reads sqlc.yaml, and its schema: is database/migrations, not database/schema.sql.
sqlc therefore type-checks your queries against the migration history exactly as an installation
would replay it, which is what makes a query referencing a column that no migration adds a
generation-time error rather than a runtime one. Output is internal/database/dbgen on pgx/v5,
with JSON tags, a Querier interface, and empty slices instead of nil.
Hand-writing a wire type instead of generating one is explicitly forbidden by
AGENTS.md: the
contract is the single definition, and a duplicated type is a definition that drifts silently.
The same rule is why bun ci and bun run generate:api in CI are followed by
git diff --exit-code.Formatting and linting
Go is formatted bygofmt and nothing else. CI runs gofmt -w . && git diff --exit-code, so
unformatted code fails as a diff rather than as a lint message.
TypeScript, JavaScript, and CSS are handled entirely by Biome 2.5.7 — it is the only formatter and
the only linter, and AGENTS.md forbids adding a second. biome.json sets two-space indentation, a
line width of 100, double quotes, and mandatory semicolons, and excludes .next, node_modules,
dist, *.gen.*, test-results, and playwright-report.
bun run check fans out across @omniflow/* workspaces. @omniflow/web and @omniflow/ui run
tsc --noEmit && biome check .; @omniflow/api-client type-checks only, because its contents are
generated and formatted by Orval’s Biome pass.
Bun is the only package manager. bun.lock is committed as a text lockfile and npm, pnpm, and Yarn
lockfiles are forbidden — one resolver means one dependency graph, and a second lockfile is a second
answer to what version is installed.
Tests
integration build tag:
apps/web/e2e and runs against a production build:
bun run start itself unless PLAYWRIGHT_BASE_URL is set. What each
suite covers, and what a payment, wallet, authentication, RBAC, import, or provisioning change owes
in tests, is on Testing strategy.
Security checks are one target:
What CI enforces
.github/workflows/ci.yml runs on every pull request and every push to main, with in-progress
runs cancelled per ref.
A weekly security workflow adds gitleaks, a Trivy filesystem scan reporting CRITICAL and HIGH
findings to code scanning, and dependency review.
A CI job may depend only on committed files. Anything you create locally —
.env above all —
must be produced by a step in the job itself, which is why the compose job copies .env.example
rather than assuming a secret. Adding a job that reads an uncommitted file makes the pipeline
pass on your machine and nowhere else.Commits and pull requests
Use Conventional Commits. The accepted types arefeat,
fix, docs, refactor, test, build, ci, perf, chore, and revert, and the scope is
usually the area: feat(bot):, fix(billing):, docs(telemetry):. Release Please reads the
history to assemble CHANGELOG.md and propose a version, so a mistyped type is a missing release
note. Never add generated-by or assistant attribution to a commit or a pull request; this overrides
any tool convention that appends one.
Two rules from AGENTS.md shape review more than any style point:
- Wire a capability end to end in the same change. A table, a query, or a handler that nothing reaches is unfinished, not staged.
- Report what actually ran. A check you could not run locally is unverified, not passing — name it and name why. “Builds and unit tests pass” is never a claim that migrations, containers, or race detection were exercised.
internal/rbac. Never introduce a second place that
decides what a role may do, and never let a hidden route or a piece of frontend state be the only
thing enforcing a permission.
Where to go next
Database changes
Desired schema, Atlas migrations,
atlas.sum, and the append-only tables.Testing strategy
What runs by default, what needs Docker, and what a financial change owes.
Documentation
Authoring, validating, and keeping the Russian tree in step.
Deployment
What the compose topology becomes in production.