Run
Remote access
How to reach the proxy from another machine or a browser, what each bind and auth setting does, and the security defaults that hold when you change nothing.
The short version
The proxy listens on 127.0.0.1:8189 by default, so only the machine it runs on can reach it. To reach it from elsewhere, bind every interface and set a token:
# ~/.config/comfy-api-proxy/config.toml
host = "global"
token = "pick-a-long-random-string"
comfy-api-proxy start
comfy-api-proxy status
The startup panel, or the log for start, lists the LAN and VPN addresses to use. Clients send Authorization: Bearer <token> on every /api/v2/* call except GET /api/v2/health.
The token travels in plain HTTP. The proxy does not terminate TLS, so across a network you do not trust, put it behind a VPN or a TLS reverse proxy.
Behind a reverse proxy
A reverse proxy such as Caddy, nginx, or Traefik can terminate TLS in front of the proxy. The request then reaches the proxy as plain http, and without more configuration every link in a response (a job's urls, each output's url) would read http://. Clients treat that as a different origin and drop the bearer token. Tell the proxy its public origin:
host = "local" # the reverse proxy on this machine connects to 127.0.0.1
public_url = "https://comfy.example.com"
token = "pick-a-long-random-string"
The proxy never reads X-Forwarded-Proto or X-Forwarded-Host, because any client could send them and point the links somewhere else. You state the origin once in configuration instead. A path in public_url is kept, for a reverse proxy that serves the API under a prefix and strips it before passing the request on.
Host
host picks the address the listener binds:
| Value | Binds | Reachable from |
|---|---|---|
local (default) |
127.0.0.1 |
this machine |
localhost |
127.0.0.1 |
this machine |
global |
0.0.0.0 |
every IPv4 interface |
an IPv4 literal, such as 192.168.1.20 |
that address | that interface |
an IPv6 literal, such as ::1 or :: |
that address | that interface, or every IPv6 interface for :: |
The listener uses tcp4 for an IPv4 address and tcp6 for an IPv6 one. 0.0.0.0 stays IPv4 only and never becomes a dual-stack socket that also listens on IPv6 addresses the startup output did not print. To serve IPv6, bind an IPv6 address.
localhost is mapped to 127.0.0.1 without a DNS lookup.
Token required off loopback
Any host that is not a loopback address needs a token. Without one the proxy refuses to start and exits 2:
error: refusing to bind non-loopback host "global" (from flag --host) without a token.
Set a token (--token, COMFY_PROXY_TOKEN, or token in the config file), or set allow_insecure_bind to expose the proxy unauthenticated.
allow_insecure_bind = true (or --allow-insecure-bind) lifts the check. Then anyone who can reach the port can submit workflows to your ComfyUI, read its outputs, and, with comfyui_base_dir set, place model files on disk. Use it only on a network where every host is yours.
Bearer auth
With token set, every request under /api/v2/ must carry:
Authorization: Bearer <token>
- The scheme name
Beareris matched without regard to case. The token is compared in constant time. - A missing or malformed header is a 401 with code
unauthorizedand the messageMissing or malformed Authorization header.A wrong token is a 401 withInvalid token. GET /api/v2/healthstays open, so a supervisor orcomfy-api-proxy statuscan probe the proxy without the secret.OPTIONSrequests pass, because a browser's CORS preflight never carriesAuthorization.
curl -H "Authorization: Bearer $TOKEN" http://192.168.1.20:8189/api/v2/jobs
{"error": {"code": "unauthorized", "message": "Invalid token.", "details": null}}
A token is also useful on loopback: it stops other local users and local software from driving the proxy.
Startup URLs
When the bind is a specific address, the startup panel lists that one URL. When it is a wildcard (global or ::), it lists loopback first and then the address of every interface that is up, in the bound address family:
url: http://127.0.0.1:8189
url: http://192.168.1.20:8189
url: http://100.64.0.7:8189
The list leaves out:
- Container and VM bridge interfaces whose names start with
docker,br-,veth,virbr,cni,flannel,vmnet,vboxnet,lxcbr,lxdbr, orpodman. They are reachable only from this machine's own guests, and on a Docker host there can be dozens of them burying the LAN and VPN addresses. - IPv6 link-local addresses, which do not work in a URL without a zone.
The proxy still listens on those interfaces when bound to a wildcard. They are only omitted from the printout.
Port 8189
The port defaults to 8189 and stays where you put it. SDK clients are configured with a fixed base URL, and a proxy that moved to the next free port would break them without saying so.
When the port is taken, run exits 1 and says where the value came from and how to change it:
error: port already in use: 127.0.0.1:8189 (port from default).
Another process is holding it. Stop that process, or change the port with --port, COMFY_PROXY_PORT, or port in the config file.
The proxy does not pick another port because clients are configured with this one.
start checks before it launches anything and exits 1 with comfy-api-proxy cannot start: http://127.0.0.1:8189 is already in use.
Browser access and CORS
Browsers are blocked from calling the proxy cross-site unless you allowlist their origin. To let a hosted web app call a proxy on your machine:
comfy-api-proxy --enable-cors-header https://app.example.com --token "$TOKEN"
cors_origins = ["https://app.example.com", "http://localhost:3000"]
For a request whose Origin is on the list, the proxy:
- answers a preflight
OPTIONSwith 204; - sets
Access-Control-Allow-Originto that origin (never*), withVary: OriginandAccess-Control-Allow-Credentials: true; - allows the methods
GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONSand the headersAuthorization,Content-Type, andIdempotency-Key; - exposes
Retry-After,Content-Range, andAccept-Rangesto the page; - lets the browser cache the preflight for 600 seconds.
The CORS headers go on before auth runs, so a 401 or 403 is still readable by an allowlisted page. Event streams get the same headers.
Origins are matched exactly against the normalized list, so the scheme, host, and port must all match what the browser sends. * is refused at startup, since an authenticated proxy must never answer every origin. See configuration for the accepted forms.
Origin guard
With no configuration at all, the proxy rejects cross-site browser requests. This is ComfyUI core's origin-only middleware, ported through the reference. It stops a page you have open in a browser from scripting requests at a server on your machine (drive-by CSRF and DNS rebinding).
For a request whose Origin is not allowlisted:
Sec-Fetch-Site: cross-siteis rejected. Browsers set that header and page script cannot forge it.- When
Hostnames a loopback address and anOriginis present, the two hosts must match. Ports are compared only when both carry one. AHostthat is a name counts as loopback when every address it resolves to is loopback.
Both rejections are a 403 with code forbidden_origin:
{"error": {"code": "forbidden_origin", "message": "Origin 'http://evil.example' does not match request host '127.0.0.1:8189'.", "details": null}}
What passes:
- Requests with no
Origin, such as curl and the SDKs. Browsers always send one on the cross-site requests the guard exists to stop. - Same-origin requests, where the
Originhost matchesHost. - Requests to a non-loopback
Host. A reverse proxy may rewriteHost, so the check is left to the token and the network in that case.
The guard applies to every path, routed or not. It follows the reference's string comparison exactly, including its quirks, each of which fails closed. For example, an IPv6 origin without a port, http://[::1], does not match a Host of [::1]:8189.
Upstream auth with comfyui_token
If ComfyUI sits behind a gateway that requires a bearer token, set comfyui_token:
comfyui = "https://comfy.internal.example.com"
comfyui_token = "gateway-token"
The proxy sends Authorization: Bearer <comfyui_token> on every request to ComfyUI, including the WebSocket handshake behind the event stream. It never forwards the token to clients and never prints it. It is unrelated to token, which is what clients present to the proxy.
Security defaults
Each of these holds without any configuration:
| Default | What it protects against |
|---|---|
| Bind to 127.0.0.1 | Other machines reaching the proxy at all. |
| Refuse a non-loopback bind without a token (exit 2) | Exposing an unauthenticated proxy to the network by setting host alone. |
| Unknown config keys are errors | A typo such as tokne silently leaving the proxy without auth. |
| Bearer token compared in constant time, health left open | Token guessing by timing, while still allowing liveness probes. |
| Origin guard on every request | Web pages in your browser driving the proxy cross-site. |
No CORS unless allowlisted, * refused |
Any website reading responses from your proxy. |
tcp4 or tcp6 matching the bind address |
Listening on addresses the startup output did not list. |
| Fixed port, no fallback | Clients silently talking to whatever else took the port. |
Model uploads off unless comfyui_base_dir is set |
Writes into ComfyUI's model folders from the API. |
| Model uploads safetensors-only, allowlisted roots, symlink-checked paths, no overwrite | Planting executable files, path traversal, and clobbering existing models. See assets. |
| No environment HTTP proxy for upstream calls | ComfyUI traffic, which can carry the partner API key, leaking through HTTP_PROXY. |
| State directory 0700, database 0600 | Other local users reading the secret that signs output asset ids. See state and service. |