X-Forwarded-For input.
Maintained examples
Both are kept working and both are validated in CI.
The Traefik example runs as a compose overlay:
Hosts
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.
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.