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

# Development

> Set up the workspace, run the generators, start the stack locally, and pass every check CI runs

Omniflow is one repository holding one Go module and one Bun workspace. The Go module builds three
binaries — `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](https://omniflow-vpn.mintlify.site/contributing/database.md).

The canonical engineering rules live in `AGENTS.md` at the repository root. This page is how you
carry them out.

## What you need installed

| Tool | Version | Why |
| - | - | - |
| Go | 1.26.10 | `go.mod` requires 1.26.10; CI and `build/go.Dockerfile` both pin 1.26.10 |
| Bun | 1.3.14 | Declared as `packageManager` in `package.json`; used for installs and every workspace script |
| Docker with Compose v2 | — | Runs PostgreSQL 18, Valkey 9, the migration step, and the three Go services |
| Atlas CLI | 1.3.0 to match the compose image | Only needed to author or apply migrations outside compose |
| Node | 26 or newer | Only to run a built Next.js server outside a container; Bun runs the scripts |

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](https://omniflow-vpn.mintlify.site/architecture/data-ownership.md) 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**

```bash theme={null}
git clone https://github.com/kewldan/omniflow.git
cd omniflow
go mod download
bun 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**

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

`.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](https://omniflow-vpn.mintlify.site/getting-started/configuration.md).

**3. Start the backend stack**

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

That is `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**

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

A healthy API answers `{"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**

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

That is `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](https://omniflow-vpn.mintlify.site/operations/admin-panel.md).

`make down` stops everything, including the web profile.

> [!NOTE]
>
> 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](https://omniflow-vpn.mintlify.site/operations/jobs-and-queues.md#the-queue-schema-ships-with-the-migrations).

## Working on the frontend outside compose

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

This runs `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](https://omniflow-vpn.mintlify.site/operations/reverse-proxy.md). 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

| Path | Contains |
| - | - |
| `cmd/api`, `cmd/bot`, `cmd/worker` | The three process entrypoints; see [Architecture overview](https://omniflow-vpn.mintlify.site/architecture/overview.md) |
| `internal/` | Domain packages, transport, adapters, and the generated `database/dbgen` |
| `api/openapi.yaml` | The REST contract, OpenAPI 3.0.3 |
| `database/` | `migrations/`, the generated `schema.sql` snapshot, and `queries/` for sqlc |
| `apps/web` | The Next.js application serving both panels |
| `packages/api-client` | Generated Fetch, SWR, and Zod bindings |
| `packages/ui` | Shared shadcn primitives used by both surfaces. No native `<select>`, date, or time input: use `Select`, a `Command`-based combobox, or `DateTimeField` |
| `docs/` | This documentation; see [Documentation](https://omniflow-vpn.mintlify.site/contributing/documentation.md) |
| `deploy/proxies` | Caddy and Traefik examples validated by CI |

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

| Change this | Run this | Regenerates |
| - | - | - |
| `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` and `schemas.ts` |
| `database/queries/*.sql` or a migration | `go tool sqlc generate` | `internal/database/dbgen` |
| A migration | `sh tools/schema-snapshot.sh` | `database/schema.sql` |

`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](https://omniflow-vpn.mintlify.site/contributing/database.md).

### What each one produces

**oapi-codegen** emits package `apigen` 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.

> [!NOTE]
>
> 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 by `gofmt` 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`.

```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` 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

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

The default Go suite runs with no Docker daemon and no network, which is the property that keeps it
actually being run. Everything that needs infrastructure lives behind the `integration` build tag:

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

That suite starts PostgreSQL through Testcontainers and applies the committed migration files in
order, so it is a migration gate as well as a repository gate. Browser coverage lives in
`apps/web/e2e` and runs against a production build:

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

The Playwright config starts `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](https://omniflow-vpn.mintlify.site/contributing/testing.md).

Security checks are one target:

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

## What CI enforces

`.github/workflows/ci.yml` runs on every pull request and every push to `main`, with in-progress
runs cancelled per ref.

| Job | Gate |
| - | - |
| Go | Regenerates oapi-codegen and sqlc output, then `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` |
| End-to-end (Playwright) | Builds, installs Chromium and WebKit, runs the suite; uploads the report as an artefact on failure |
| Integration (Testcontainers) | `go test -tags=integration -race -timeout=20m ./internal/integrationtest/...` |
| Atlas migrations | `atlas migrate validate`, then per baseline: apply the baseline, upgrade to head, apply head again to prove it is a no-op |
| Compose and reverse proxies | Copies `.env.example` to `.env`, validates the base compose file and the Traefik overlay, validates the Caddyfile, and builds the `api`, `bot`, and `worker` images |
| Mintlify docs | `mint validate` and `mint broken-links --check-anchors` |
| Conventional PR title | The pull request title must be a Conventional Commit |

A weekly security workflow adds gitleaks, a Trivy filesystem scan reporting CRITICAL and HIGH
findings to code scanning, and dependency review.

> [!NOTE]
>
> 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](https://www.conventionalcommits.org/). The accepted types are `feat`,
`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.

Authorization is compiled in and lives only in `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](https://omniflow-vpn.mintlify.site/contributing/database.md)**
>
> Desired schema, Atlas migrations, `atlas.sum`, and the append-only tables.

> **[Testing strategy](https://omniflow-vpn.mintlify.site/contributing/testing.md)**
>
> What runs by default, what needs Docker, and what a financial change owes.

> **[Documentation](https://omniflow-vpn.mintlify.site/contributing/documentation.md)**
>
> Authoring, validating, and keeping the Russian tree in step.

> **[Deployment](https://omniflow-vpn.mintlify.site/operations/deployment.md)**
>
> What the compose topology becomes in production.


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