localhost, uses the credentials in compose.yaml, and terminates no TLS. For a server that faces
the internet, read Deployment and Reverse proxy
before you expose anything.
Before you start
What each service is
compose.yaml defines seven services. Only api and web publish a host port; PostgreSQL and
Valkey stay on the internal Compose network.
The
migrate service has the development DSN
postgres://omniflow:omniflow@postgres:5432/omniflow?sslmode=disable hard-coded in its command
and does not read .env. If you change the database credentials, change them in compose.yaml
as well, or migrations will be applied somewhere other than where the application looks.Configure the installation
All project-owned configuration is environment variables with anAPP_ prefix, read once at
startup. There is no config file and no live reload: changing a value means restarting the process
that reads it.
1
Copy the example file
2
Generate the two secrets
APP_DATA_ENCRYPTION_KEY must decode to exactly 32 bytes or startup fails with
APP_DATA_ENCRYPTION_KEY must be base64-encoded 32 bytes. It seals contact values, TOTP
secrets, OIDC client secrets, provider credentials, and the short-lived sign-in flow cookie —
losing it means losing access to all of them.APP_OPERATOR_TOKEN is the bearer credential for the entire /v1/admin prefix, compared in
constant time. Any random high-entropy string will do.3
Fill in the values the processes require
Other values are validated the same way:
APP_DEFAULT_CURRENCY must match ^[A-Z]{3}$ after
uppercasing, APP_TELEGRAM_WEBHOOK_URL must be HTTPS, and APP_TELEMETRY_ENDPOINT must be an
HTTPS URL while telemetry is enabled.4
Allow plain-HTTP cookies for a local stack
true, which is correct for anything with TLS in front of it. The API logs
admin session cookie is not marked Secure; use this only for local development when you turn
it off, and that warning is accurate: without Secure, the session cookie can be captured from
any unencrypted request to the same host. Set both back to true before the installation
leaves your machine.Bring the stack up
migrate runs first and exits; api, bot, and worker start once it has completed successfully
and Valkey reports healthy. Then start the web application, which serves both the customer account
and the operator workspace:
- API —
http://localhost:8080 - Customer account —
http://localhost:3000 - Operator workspace —
http://localhost:3000/admin
make dev is the first command without migrate in the argument list, make up is everything
including the web profile, and make down stops it all. Run make help for the full index of
targets.
Create the first owner
A fresh installation has no operator account, so the sign-in page would be a dead end. On start, while no operator exists, the API issues a one-time setup token and writes it to its own log at warning level. It goes to the log rather than to an HTTP response because at that moment nothing can authenticate, and an endpoint that handed the token out would hand it to anyone who asked.1
Read the token from the API log
POST /v1/panel/bootstrap answers 409 bootstrap_closed permanently.2
Redeem it at /admin/setup
Open
http://localhost:3000/admin/setup. The form asks for the setup token, a display name of
1–80 characters, an email address, and a password of 12–256 characters. There are no
composition rules; length is the only requirement, and the browser form mirrors exactly what the
API enforces.Opening /admin/login on an installation that still needs its first owner redirects here
automatically, and this page redirects back to the sign-in screen the moment an account exists,
so a stale tab cannot sit on a form that can no longer be submitted.A refusal is specific: 422 weak_password for a password that is too short,
409 email_in_use for an address already registered, 409 bootstrap_closed once bootstrap has
completed.3
Sign in
You are redirected to
/admin/login. Enter the address and password. The account has no second
factor yet, so you land on the dashboard at /admin.Sign-in answers identically for a wrong password, an unknown address, and a disabled account —
all three are 401 invalid_credentials — so the endpoint cannot be used to discover which
addresses have operator accounts. It is rate limited to 10 attempts per minute per address and
30 per minute per client IP, and if Valkey is unreachable the check fails closed.Enable two-factor authentication
The first owner holds every permission in the installation, including the only role that can create other operators. Give the account a second factor before you do anything else with it.1
Start enrolment
Open
/admin/security and press Enable. The response carries a TOTP secret and an
otpauth:// URI. The secret is shown as text as well as in the QR code, because an
authenticator running on the same device cannot scan the screen it is on.The label your authenticator shows comes from APP_ADMIN_TOTP_ISSUER, which defaults to
Omniflow.2
Confirm the first code
Type the six-digit code and submit. Until a code is verified the secret is stored unconfirmed
and satisfies no sign-in challenge, so a half-finished enrolment can never lock you out. A wrong
code answers
422 invalid_code; confirming without starting answers 409 totp_not_started.3
Store the recovery codes
Confirmation returns ten single-use recovery codes. They are displayed once and never again —
regenerating them replaces the whole set and invalidates every unused code. The sign-in
challenge field accepts 6–24 characters and is a text input rather than a numeric one, because
a recovery code contains letters.
Prove that it works
Liveness and readiness answer different questions and are separate endpoints on purpose: a process that is running but cannot reach PostgreSQL should be removed from a load balancer, not restarted./healthz is the same handler as /livez, kept for deployments that already probe it.
200:
503 with "status": "degraded" and the failing
check carrying "error": "timeout" or "error": "unavailable" plus a consecutiveFailures count.
Those two words are the only failure values there are: a pgx or HTTP error routinely contains a DSN,
a host name, or a credential, so the probe reports a classification instead. The remnawave probe
is an authenticated call, which means a revoked API token shows up here as an unhealthy dependency
rather than as a mysterious provisioning failure later.
Which probes each process registers differs, and it matters:
Signed in as an operator, the same information is on
/admin under System → Health
(GET /v1/panel/overview/health, permission system.read), together with payment-provider and
digital-goods-provider health that the HTTP probe does not cover.
What does not work on a fresh clone
These are current facts aboutmain, not configuration mistakes. Read them before you conclude that
your installation is broken.
- The durable job queue has no schema. Omniflow enqueues background work with River, but no
committed migration creates River’s own tables and nothing in the stack runs River’s migrator. On
a database migrated only by
docker compose up migrate,workertherefore exits at startup loggingstart River, and any settlement — which inserts a fulfillment job in the same transaction as the money — rolls back. A purchase cannot complete until that migration exists. See Your first sale for exactly where a test purchase stops, and Jobs and queues for what the queue is meant to do. - The transactional outbox is written and never published. Three topics are inserted, nothing
ever sets
published_at, and no dispatcher exists. The outbox counters on the dashboard and atGET /v1/admin/outboxtherefore grow permanently,APP_RETENTION_OUTBOX_DAYSdeletes nothing, and the “outbox lag” attention item on/adminis expected rather than diagnostic. - There is no tracer provider. The code opens spans across HTTP, Telegram updates, jobs, and
SQL, but no exporter is installed and no
OTEL_*variable is read anywhere in the repository. Setting the standard OpenTelemetry variables changes nothing; the global provider stays a no-op. Metrics and logs are what an installation actually emits — see Observability. - Every binary reports
"version":"dev". Nothing stamps a version at build time, including the released images, so/livez, the diagnostics bundle, and the telemetry heartbeat all saydev. - The panel’s settings sections do not configure the runtime. Saving a section stores a document; the Remnawave connection, the bot, the operator group, maintenance policy, telemetry, backups, and the payment adapters all come from environment variables read at startup. See Installation settings.
- Nothing restarts a crashed process. No core Compose service declares a
restart:policy, so a process that exits stays down until somebody notices. Add one for anything long-lived. - A browser-uploaded support attachment has no volume.
APP_SUPPORT_ATTACHMENT_DIRdefaults to/var/lib/omniflow/support-attachmentsand is read by bothapiandworker, butcompose.yamlmounts a volume there for neither. Files live in the container’s writable layer until it is rebuilt, and the worker cannot reclaim files it cannot see.
Next steps
Your first sale
Create a plan, price a version, configure a provider, connect the bot, and run a test purchase.
Configuration reference
Every variable, its default, which process reads it, and what it changes.
Deployment
Volumes, image tags, migrations, upgrades, and the supported topology.
Reverse proxy
TLS, body limits, and keeping
/metrics and /readyz off the public host.Backup and restore
Turn on encrypted backups before there is anything worth losing.
Anonymous telemetry
The exact payload, and the one variable that stops every telemetry request.