Tunnels Guide
Source of truth:
src/lib/{cloudflaredTunnel,ngrokTunnel,tailscaleTunnel}.ts,src/app/api/tunnels/Last updated: 2026-06-28 — v3.8.40
OmniRoute can expose its local server (http://localhost:20128) to the public
internet via three tunnel backends. This is useful for:
- OAuth callbacks from cloud providers (Antigravity, Gemini, Cursor) that need a publicly reachable redirect URL.
- Sharing your local instance with teammates without deploying a VM.
- Mobile, remote, or cross-network testing.
All three backends are managed in-process — OmniRoute starts/stops the underlying binary or SDK from the dashboard or REST API. No reverse-proxy or systemd setup is required.
Backends at a glance
Section titled “Backends at a glance”| Backend | Persistence | Cost | Setup |
|---|---|---|---|
| Cloudflare Quick Tunnel | Ephemeral (URL changes each restart) | Free | Zero — auto-installs cloudflared |
| ngrok | Stable while a paid plan or fixed domain is configured | Free tier + paid | Requires ngrok account + authtoken |
| Tailscale Funnel | Stable per node within your tailnet | Free for personal | Requires Tailscale install + login + Funnel ACL |
The implementations live in src/lib/cloudflaredTunnel.ts,
src/lib/ngrokTunnel.ts, and src/lib/tailscaleTunnel.ts. All three return a
common-shaped status object with phase, running, publicUrl, apiUrl,
targetUrl, and lastError fields, so the dashboard can render them uniformly.
1. Cloudflare Tunnel (Quick Tunnel + Named Tunnel)
Section titled “1. Cloudflare Tunnel (Quick Tunnel + Named Tunnel)”src/lib/cloudflaredTunnel.ts runs cloudflared as a child process. It supports
two modes, selected by whether a named-tunnel config is provided:
- Quick tunnel (default). Runs
cloudflared tunnel --url http://localhost:<apiPort>and parses the assigned*.trycloudflare.comURL from stdout. URLs are ephemeral and change on every restart. - Named tunnel (opt-in). When
CLOUDFLARED_CONFIGpoints at a locally-managed cloudflaredconfig.yml, OmniRoute runscloudflared tunnel --no-autoupdate --config <path> run, giving you a stable, named hostname. The config supplies the tunnel UUID,credentials-file, andingressrouting, so no--urlis passed and no Zero Trust dashboard token is required.runreads credentials from the config’s absolutecredentials-filepath — nocert.pemis needed (that is only used for tunnel lifecycle management).
Key behaviors:
- Auto-install. On first use, OmniRoute downloads the latest
cloudflaredbinary from the official GitHub releases (managed install lives underDATA_DIR/cloudflared/). SHA256 of the downloaded asset is verified against the release manifest before execution. - Process supervision. The cloudflared PID and resolved URL are persisted to
quick-tunnel-state.jsonso the dashboard can resume status across reloads.
Named tunnel setup (stable hostname)
Section titled “Named tunnel setup (stable hostname)”-
Create a locally-managed tunnel with the cloudflared CLI (one-time):
Terminal window cloudflared tunnel logincloudflared tunnel create omniroutecloudflared tunnel route dns omniroute ai.example.com -
Write a
~/.cloudflared/config.ymlrouting your hostname to OmniRoute’s local API port (default 20128):tunnel: <UUID-from-create>credentials-file: /home/you/.cloudflared/<UUID>.jsoningress:- hostname: ai.example.comservice: http://127.0.0.1:20128- service: http_status:404 -
Point OmniRoute at the config and (re)start the tunnel:
Terminal window export CLOUDFLARED_CONFIG="/home/you/.cloudflared/config.yml"# optional — overrides the hostname OmniRoute reports; otherwise read from the# config's first ingress rule:# export CLOUDFLARED_HOSTNAME="ai.example.com"Enable the tunnel the same way as a quick tunnel (REST / dashboard / CLI below). A named tunnel emits no public URL to scrape, so readiness is detected from cloudflared’s registered edge connection, and
publicUrl/apiUrlare reported fromCLOUDFLARED_HOSTNAME(or the config’s first ingress hostname).
Enable / disable via REST
Section titled “Enable / disable via REST”The endpoint uses an {action: "enable" | "disable"} body, not separate
start/stop paths. Management auth (admin session or admin API key) is
required.
# Enablecurl -X POST http://localhost:20128/api/tunnels/cloudflared \ -H "Content-Type: application/json" \ -H "Cookie: auth_token=..." \ -d '{"action":"enable"}'
# Statuscurl http://localhost:20128/api/tunnels/cloudflared \ -H "Cookie: auth_token=..."
# Disablecurl -X POST http://localhost:20128/api/tunnels/cloudflared \ -H "Content-Type: application/json" \ -H "Cookie: auth_token=..." \ -d '{"action":"disable"}'Or via dashboard: Settings → Tunnels → Cloudflare.
Optional env vars
Section titled “Optional env vars”| Variable | Purpose |
|---|---|
CLOUDFLARED_BIN |
Override the binary path. If set and valid, OmniRoute uses it instead of downloading. |
CLOUDFLARED_PROTOCOL / TUNNEL_TRANSPORT_PROTOCOL |
Transport protocol (default http2; also quic, auto). |
CLOUDFLARED_CONFIG |
Path to a locally-managed cloudflared config.yml. When set, OmniRoute runs a named/persistent tunnel (tunnel --config <path> run) instead of a quick tunnel. |
CLOUDFLARED_HOSTNAME |
Overrides the named tunnel’s reported public hostname (e.g. ai.example.com). When unset, read from the config’s first ingress hostname. |
2. ngrok
Section titled “2. ngrok”src/lib/ngrokTunnel.ts uses the @ngrok/ngrok SDK (in-process, no CLI
subprocess). The native module is imported lazily on first start so platforms
without prebuilt binaries do not break the app at boot.
Prerequisites
Section titled “Prerequisites”- Sign up at https://ngrok.com.
- Copy your authtoken from the ngrok dashboard.
- Provide it either via:
.env:NGROK_AUTHTOKEN=<token>, or- Dashboard: Settings → Tunnels → ngrok, or
- REST body (one-shot):
{"action":"enable","authToken":"<token>"}.
If neither is configured, status returns phase: "needs_auth".
Enable / disable via REST
Section titled “Enable / disable via REST”# Enable (uses NGROK_AUTHTOKEN from env)curl -X POST http://localhost:20128/api/tunnels/ngrok \ -H "Content-Type: application/json" \ -H "Cookie: auth_token=..." \ -d '{"action":"enable"}'
# Enable with inline tokencurl -X POST http://localhost:20128/api/tunnels/ngrok \ -H "Content-Type: application/json" \ -H "Cookie: auth_token=..." \ -d '{"action":"enable","authToken":"2abc..."}'
# Statuscurl http://localhost:20128/api/tunnels/ngrok \ -H "Cookie: auth_token=..."
# Disablecurl -X POST http://localhost:20128/api/tunnels/ngrok \ -H "Content-Type: application/json" \ -H "Cookie: auth_token=..." \ -d '{"action":"disable"}'The response includes the assigned publicUrl (e.g.
https://abcd-1234.ngrok-free.app). Custom domains, regions, and policy rules
must be configured in the ngrok dashboard — OmniRoute itself only forwards the
local target URL to the SDK.
3. Tailscale Funnel
Section titled “3. Tailscale Funnel”src/lib/tailscaleTunnel.ts orchestrates the system tailscale CLI to expose
the local API port via Funnel (Tailscale’s public-internet egress for serve).
It supports the full lifecycle: install, login, daemon start, enable, disable.
The implementation invokes tailscale funnel --bg <port> (background mode). The
public URL has the shape https://<machine>.<tailnet>.ts.net/.
Prerequisites
Section titled “Prerequisites”- Install Tailscale (or let OmniRoute do it — see
installendpoint below). - Sign in (
tailscale loginor via OmniRoute’sloginendpoint). - Enable Funnel for your tailnet in the Tailscale admin console: https://login.tailscale.com/admin/settings/features.
On Linux and macOS the daemon (tailscaled) requires sudo to control. The
POST endpoints accept an optional sudoPassword field which is forwarded to
OmniRoute’s MITM password cache (getCachedPassword / setCachedPassword) for
the duration of the call. Windows uses the default service install at
C:\Program Files\Tailscale\tailscale.exe.
REST endpoints
Section titled “REST endpoints”Tailscale has a richer surface than the other backends because installation, login, daemon, and tunnel are separate concerns.
| Endpoint | Method | Purpose |
|---|---|---|
/api/tunnels/tailscale |
GET |
Aggregated tunnel status (phase, tunnelUrl, apiUrl, etc.) |
/api/tunnels/tailscale/check |
GET |
Lower-level check: installed? logged in? daemon running? |
/api/tunnels/tailscale/install |
POST |
Install Tailscale (SSE-streamed progress events) — Linux/macOS |
/api/tunnels/tailscale/start-daemon |
POST |
Start tailscaled on Linux/macOS |
/api/tunnels/tailscale/login |
POST |
Begin login flow; returns authUrl to open in a browser |
/api/tunnels/tailscale/enable |
POST |
Start the Funnel for the API port |
/api/tunnels/tailscale/disable |
POST |
Stop the Funnel |
All Tailscale endpoints require management auth (see routeUtils.ts :: requireTailscaleAuth).
Example enable:
curl -X POST http://localhost:20128/api/tunnels/tailscale/enable \ -H "Content-Type: application/json" \ -H "Cookie: auth_token=..." \ -d '{"sudoPassword":"<linux-pwd>","port":20128}'If Funnel is not enabled in the admin console, the response includes
funnelNotEnabled: true plus an enableUrl to open in a browser.
Optional env vars
Section titled “Optional env vars”| Variable | Purpose |
|---|---|
TAILSCALE_BIN |
Override the tailscale binary path |
Endpoint summary
Section titled “Endpoint summary”| Endpoint | Method | Body | Auth |
|---|---|---|---|
/api/tunnels/cloudflared |
GET |
— | management |
/api/tunnels/cloudflared |
POST |
{action: "enable" | "disable"} |
management |
/api/tunnels/ngrok |
GET |
— | management |
/api/tunnels/ngrok |
POST |
{action, authToken?} |
management |
/api/tunnels/tailscale |
GET |
— | management |
/api/tunnels/tailscale/check |
GET |
— | management |
/api/tunnels/tailscale/install |
POST |
{sudoPassword?} (SSE) |
management |
/api/tunnels/tailscale/start-daemon |
POST |
{sudoPassword?} |
management |
/api/tunnels/tailscale/login |
POST |
{hostname?} |
management |
/api/tunnels/tailscale/enable |
POST |
{sudoPassword?, hostname?, port?} |
management |
/api/tunnels/tailscale/disable |
POST |
{sudoPassword?} |
management |
There is no central /api/settings/tunnels endpoint — each backend is
independent.
OAuth callback considerations
Section titled “OAuth callback considerations”When you expose OmniRoute through a tunnel, the dashboard and OAuth flows must
build callback URLs against the public hostname, not localhost. Otherwise
the OAuth provider redirects the user back to a URL its servers cannot reach,
and the handshake fails.
Dashboard edits and settings saves do not require pinning the tunnel hostname in
NEXT_PUBLIC_BASE_URL. The authenticated dashboard sends same-origin unsafe
requests with a session-bound CSRF token, so ephemeral Cloudflare Quick Tunnel
hosts can still be used for normal UI management after logging in.
Set:
NEXT_PUBLIC_BASE_URL=https://<your-tunnel-host>and restart OmniRoute before initiating OAuth. For ephemeral Cloudflare Quick Tunnels the URL changes after every restart, so prefer ngrok with a reserved domain or Tailscale Funnel for production OAuth use.
Health and monitoring
Section titled “Health and monitoring”The dashboard surfaces tunnel state under Settings → Tunnels:
- Active backend(s) and current
phase(stopped,starting,running,needs_auth,error). - The current public URL and the derived API URL (
<publicUrl>/v1). - The local target URL the tunnel is forwarding to.
- Last error message, if any.
For programmatic monitoring poll the per-backend GET endpoints. Running more
than one backend simultaneously is allowed; OmniRoute will track each
independently.
Troubleshooting
Section titled “Troubleshooting”“cloudflared binary not found”
Section titled ““cloudflared binary not found””OmniRoute attempts to auto-install on first use. If the install is blocked
(restricted network, no GitHub access), download cloudflared manually from
https://github.com/cloudflare/cloudflared/releases and set
CLOUDFLARED_BIN=/path/to/cloudflared.
“ngrok: authtoken required”
Section titled ““ngrok: authtoken required””phase: "needs_auth" means no authtoken was found. Set NGROK_AUTHTOKEN in
.env, configure it via the dashboard, or pass authToken in the enable POST
body.
“tailscale: funnel not enabled”
Section titled ““tailscale: funnel not enabled””When the enable response includes funnelNotEnabled: true, Funnel is disabled
for your tailnet. Open the returned enableUrl (or the admin console feature
page) and toggle Funnel on.
Tunnel URL changes break OAuth
Section titled “Tunnel URL changes break OAuth”Use ngrok with a reserved domain or Tailscale Funnel (both stable per-node). Cloudflare Quick Tunnels are ephemeral by design and not recommended for long-lived OAuth callbacks.
Permission denied on Linux/macOS for Tailscale
Section titled “Permission denied on Linux/macOS for Tailscale”tailscaled needs root. Provide sudoPassword to the relevant POST endpoint,
or run the daemon yourself (sudo systemctl start tailscaled).
See also
Section titled “See also”- PROXY_GUIDE.md — outbound proxy (1proxy, SOCKS5, HTTP) for egress traffic.
- ENVIRONMENT.md — full list of env vars including
NEXT_PUBLIC_BASE_URL. - FLY_IO_DEPLOYMENT_GUIDE.md, DOCKER_GUIDE.md — alternatives to tunneling for stable public hosting.
- Source:
src/lib/{cloudflaredTunnel,ngrokTunnel,tailscaleTunnel}.ts,src/app/api/tunnels/.
HagiCode
HagiCode is an agentic coding workspace: structured workflows, multi-agent execution, and Hero Dungeon views turn ideas into shipped software.
Turn ideas into polished, usable software with a smarter, faster, and more enjoyable agentic coding workflow.

- SmartStructured workflows turn intent into an executable path from idea to shipped change.
- EfficientMulti-agent workflows keep research, implementation, and review moving in parallel.
- FunHero Dungeon interfaces make long coding sessions visual, collaborative, and rewarding.