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

# Reverse proxy

> Use Caddy, Traefik, Nginx, or another compatible ingress.

Omniflow does not require, bundle, or automatically start a reverse proxy. Operators may choose Caddy, Traefik, Nginx, HAProxy, or a managed ingress.

The proxy must provide TLS, preserve request IDs, enforce body limits, and route API, web, Telegram webhook, and provider webhook traffic correctly. Customer and admin routes are served by the same web container. Configure trusted proxy headers explicitly; never trust arbitrary `X-Forwarded-For` input.

## Maintained examples

Both are kept working and both are validated in CI.

| Proxy | Files |
| - | - |
| Caddy | `deploy/proxies/Caddyfile` |
| Traefik | `deploy/proxies/traefik.yaml`, `traefik-dynamic.yaml`, `traefik.compose.yaml` |

The Traefik example runs as a compose overlay:

```bash theme={null}
ACME_EMAIL=ops@example.com \
WEB_DOMAIN=app.example.com \
API_DOMAIN=api.example.com \
BOT_WEBHOOK_DOMAIN=bot.example.com \
docker compose -f compose.yaml -f deploy/proxies/traefik.compose.yaml up -d
```

The overlay also stops publishing the API and web ports on the host, since the proxy fronts them.

## Hosts

| Host | Serves |
| - | - |
| `WEB_DOMAIN` | The unified customer and admin application; the admin workspace stays under `/admin`. **Also `/v1`**, routed to the API — see below |
| `API_DOMAIN` | The Go API, including provider webhooks at `/v1/payments/webhooks/{provider}` |
| `BOT_WEBHOOK_DOMAIN` | `POST /telegram/webhook` only, when the bot runs in webhook mode |

`BOT_WEBHOOK_DOMAIN` may be the same host as the API. Keeping it separate is tidier but not required.

## `/v1` must be served on the web host

Both panels call the API **same-origin**. The shared browser client keeps a base URL of `""`
(`packages/api-client/src/fetcher.ts`) and nothing calls `setBaseUrl`, so a request from the panel
goes to the web host and expects the API to answer it. Both examples therefore route `/v1` on
`WEB_DOMAIN` to the API container, with the same body limit and the same block on the operational
endpoints as the public API host.

This is not a convenience. It is what keeps the session cookie first-party, which is what lets it
stay `SameSite=Lax` and `HttpOnly` with no CORS anywhere in the stack. Serving the panel API
cross-origin instead would require `SameSite=None`, a `Secure` cookie sent on cross-site requests,
and an allow-list of origins with credentials — three moving parts protecting an operator session,
in place of one.

> [!WARNING]
>
> A proxy that serves `WEB_DOMAIN` without routing `/v1` produces a panel that looks entirely
> healthy and cannot do anything. Pages render, an anonymous visitor is still redirected to sign
> in, and the sign-in form reports *"Sign-in failed. Try again."* — because the request reached
> Next.js, which has no `/v1` route, rather than the API. If you write your own proxy
> configuration, this is the rule to carry across.

`API_DOMAIN` is still worth publishing: provider webhooks, the `/v1/admin` bearer surface, and the
Telegram webhook are called by systems that are not the browser. A single-host installation that
serves only `WEB_DOMAIN` works for both panels, but has nowhere to point a payment provider.

## Do not publish the operational endpoints

`/metrics` and `/readyz` describe installation internals: dependency health, queue depth, payment classifications. Both examples return 404 for them on the public API host. Scrape them over the internal network instead, or put them behind your own authentication.

`/livez` is safe to expose — it reports only that the process is running.

## Body limits

| Route | Limit | Why |
| - | - | - |
| API | 2 MB | The application already caps JSON bodies at 1 MB; the proxy limit is the outer guard |
| Telegram webhook | 1 MB | Telegram updates are small, and the bot caps them too |

## Do not rewrite request bodies

Provider webhook signatures are computed over the exact bytes the provider sent. A proxy that re-encodes, pretty-prints, or normalises a JSON body invalidates every signature, and Omniflow will correctly reject the result as unverified. If webhooks start failing verification after a proxy change, this is the first thing to check.

## Access logs

An access log records request paths, and a subscription link is a request path. The Traefik example turns access logging off by default for that reason. If you need it, apply a path filter you have reviewed.


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