* docs: move superpowers/research artifacts to isolated _tasks repo + docs tree cleanup
- Move docs/superpowers/{plans,specs} and docs/research/* into the gitignored,
separately-versioned _tasks/ repo; untrack the two tracked research design docs.
- Add CLAUDE.md "Planning & Research Artifacts" section overriding the superpowers
default save paths (docs/... -> _tasks/...); align REPOSITORY_MAP and
DOCUMENTATION_OVERHAUL_PLAN with the new convention.
- Drop 4 now-obsolete /api/discovery/* entries from check-docs-symbols allowlist
(stale-enforcement) and refresh code/spec path comments to _tasks/...
- Sweeps in concurrent docs-tree restructuring (root-level provider/guide docs,
compression spec cleanup, .mcp.json.example removal).
* docs: reorganize docs/ tree + fix stale facts across ~26 docs
Phase A — reorganization:
- Move 7 orphan root docs into subfolders (providers/ created; TIERS+USAGE_QUOTA→guides/;
plugins+PLUGIN_SDK→frameworks/); delete 8 obsolete/redundant docs (SUBMIT_PR superseded
by CONTRIBUTING; DOCUMENTATION_OVERHAUL_PLAN; INCIDENT_RESPONSE/PERF_BUDGETS/THREAT_MODEL;
3 ops snapshots). Rebuild README index (was missing ~40 files) + per-folder meta.json nav.
- Clean 14 dangling doc-path references in bin/ ops scripts, scripts/, workflow, tests;
fix the dockerignore-docs-coverage required-docs path (PROVIDERS→providers/CLAUDE_WEB).
Phase B — content accuracy (verified against code, not the audit summary):
- Functional: ENVIRONMENT flag defaults (INPUT_SANITIZER/MCP_ENFORCE_SCOPES=true,
COMPRESS_DESCRIPTIONS=false, dynamic heap); MCP-SERVER notion tool names (omniroute_*→
notion_*) + counts 87→94; coverage gate 75/70→60/60/60/60 (RELEASE_CHECKLIST, COVERAGE_PLAN,
ERROR_SANITIZATION, CONTRIBUTING); pre-push hook description; regenerate PROVIDER_REFERENCE (237).
- Count drift: providers 237, executors 70, migrations 106, db modules 94, oauth 19,
strategies 17, MCP 94, flags 38, TS 6.0, open-sse ~900/services 294 across architecture/
frameworks/ops docs; AUTO-COMBO 9→12 factors w/ correct DEFAULT_WEIGHTS; REASONING +2
patterns; STEALTH UA defaults; AGENT_PROTOCOLS +cursor-cloud/list-capabilities;
LANGUAGE_PACKS +id pack.
- Kept Node 20 (runtime guard accepts 20.20.2+; only engines is stricter) and MCP scopes=13
(mcpScopes.ts) — both were correct in the docs; corrected only the attribution.
* docs: finish content refresh — compression engines, CLI_TOKEN merge, metadata sweep
- Compression: document the additional built-in engines (CCR, headroom, ionizer,
session-dedup) in COMPRESSION_ENGINES; clarify LLMLingua-2 is the ultra-mode SLM
backend + cross-ref the extra engines in EXTENDING_COMPRESSION; add the id
(Indonesian) language pack to LANGUAGE_PACKS.
- AUTO-COMBO: replace the orphan 'How tiers fit' weight table (stale weights) with a
pointer to the canonical 12-factor DEFAULT_WEIGHTS table.
- Security: merge CLI_TOKEN_AUTH.md (legacy 32-char SHA-256 format) into CLI_TOKEN.md
as a 'Legacy format — still accepted' section (server accepts both HMAC + legacy),
delete CLI_TOKEN_AUTH.md, drop it from the index + security nav.
- Metadata: bump stale frontmatter (version/lastUpdated) to 3.8.40/2026-06-28 across the
doc set audited this pass, and normalize the in-body 'Last updated' header lines to match.
* fix(runtime): drop Node 20 from supported range + align all docs/diagrams/counts
- Node minimum is now 22 (aligned with package.json engines). SUPPORTED_NODE_RANGE in
src/shared/utils/nodeRuntimeSupport.ts (and the bin/ mirror) drops the 20.x line →
'>=22.22.2 <23 || >=24.0.0 <27'; getNodeRuntimeSupport now rejects Node 20 as
unsupported-major. Test updated (TDD): node-runtime-support.test.ts asserts Node 20
rejected. Docs aligned (TROUBLESHOOTING ×2, TERMUX, RELEASE_CHECKLIST, CODEBASE,
CLI-TOOLS, README, llm.txt + 42 i18n llm.txt mirrors, skills/cli-serve).
- Diagrams regenerated: mcp-tools-87 -> mcp-tools-94 (34 base + pool 6 = 94) and
auto-combo-9factor -> auto-combo-12factor (correct DEFAULT_WEIGHTS); SVGs re-rendered
via mermaid-cli; doc refs + diagrams/README updated; fixed a pre-existing broken
resilience-3layers image path.
- CLAUDE.md + AGENTS.md aligned to real counts (237 providers, 94 MCP tools / 34 base,
106 migrations, 94 db modules, 12-factor auto-combo, 17 strategies); README provider
count 231 -> 237; executor count corrected to 68 (provider executors, excl base/index)
and OAuth to 18 across architecture docs. check:docs-all now passes (0 strict drift,
0 broken links); removed dead .mcp.json.example doc link.
* fix(services): update installer Node hint to >=22.22.2 (aligned with dropped Node 20)
* docs: realign counts to current release tip after rebase
The release tip advanced while this work was in flight (Gemini CLI provider/executor
removed by #5246, plus other PRs). Re-counted against the current code and updated:
providers 237->236, executors 68->67, OAuth modules 18->17, open-sse services 294->298;
regenerated PROVIDER_REFERENCE.md (236). check:docs-all passes (0 strict drift).
* docs(changelog) + i18n: record Node 20 drop + fix nodeIncompatibleHint
- CHANGELOG: add [3.8.40] entries for the Node 20.x removal (runtime) and the docs
reorganization/accuracy audit.
- i18n: nodeIncompatibleHint across all 42 locales no longer lists Node 20.x as
supported (ASCII + CJK full-width variants), aligned with the dropped Node 20.
* fix(docs): repair CI breakages from the doc moves
- test: cli-plugin-system asserted docs/dev/plugins.md exists; the file moved to
docs/frameworks/PLUGINS.md — point the test at the new path (Unit fast-path 2/2 fix).
- frontmatter: PLUGINS.md and PLUGIN_SDK.md moved into the fumadocs-indexed
docs/frameworks/ which requires a 'title' frontmatter; the missing frontmatter
failed the Next.js MDX build (dast-smoke 'invalid frontmatter'). Added frontmatter
to both, plus the providers/ docs (consistency; that folder is not indexed).
12 KiB
title, version, lastUpdated
| title | version | lastUpdated |
|---|---|---|
| Tunnels Guide | 3.8.40 | 2026-06-28 |
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
| 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)
src/lib/cloudflaredTunnel.ts runs cloudflared tunnel --url http://localhost:<apiPort> as a child process and parses the assigned
*.trycloudflare.com URL from stdout.
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. - Quick-tunnel only. The current implementation runs only the
--url-style quick tunnel. Named/persistent tunnels (cloudflared tunnel login+cloudflared tunnel route dns ...) are not orchestrated by OmniRoute. URLs are ephemeral and will change every restart. - Process supervision. The cloudflared PID and resolved URL are persisted to
cloudflared-state.jsonso the dashboard can resume status across reloads.
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.
# Enable
curl -X POST http://localhost:20128/api/tunnels/cloudflared \
-H "Content-Type: application/json" \
-H "Cookie: auth_token=..." \
-d '{"action":"enable"}'
# Status
curl http://localhost:20128/api/tunnels/cloudflared \
-H "Cookie: auth_token=..."
# Disable
curl -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
| 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). |
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
- 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
# 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 token
curl -X POST http://localhost:20128/api/tunnels/ngrok \
-H "Content-Type: application/json" \
-H "Cookie: auth_token=..." \
-d '{"action":"enable","authToken":"2abc..."}'
# Status
curl http://localhost:20128/api/tunnels/ngrok \
-H "Cookie: auth_token=..."
# Disable
curl -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
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
- 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
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
| Variable | Purpose |
|---|---|
TAILSCALE_BIN |
Override the tailscale binary path |
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
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.
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
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
"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"
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"
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
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
tailscaled needs root. Provide sudoPassword to the relevant POST endpoint,
or run the daemon yourself (sudo systemctl start tailscaled).
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/.