App Not Working Behind the Tunnel?

Common issues when exposing local apps through a reverse tunnel, and how to fix them.

Why Some Apps Break Behind a Tunnel

When you tunnel a local app, the browser accesses it through a different domain (e.g. https://free-abc123.skytunnel.dev instead of localhost:3000). Most apps work fine with this. Some don't, because they have security mechanisms that reject requests from unknown origins.

This is not a Skytunnel bug. It happens with any tunneling service - ngrok, Cloudflare Tunnel, Tailscale Funnel, etc. The fix is always on the app side.

Cause 1: CORS / CSRF Origin Rejection

Many web frameworks validate the Origin header on POST/PUT/DELETE requests. If your app has a hardcoded allowlist of origins (e.g. only localhost:3000 and your production domain), requests from the tunnel URL will be rejected with 403 Forbidden.

Fix: Add the tunnel origin to your app's CORS/CSRF allowlist. Best practice is to use an environment variable like ALLOWED_ORIGINS so you can add tunnel URLs without changing code. Example: ALLOWED_ORIGINS=http://localhost:3000,https://free-abc123.skytunnel.dev

Cause 2: Cookie Domain Mismatch

Auth cookies (session tokens, JWTs) are scoped to the domain they were set on. If you log in via localhost:3000, your cookies are set for localhost. When you access the same app via the tunnel URL, the browser does not send those cookies - different domain.

Fix: Log in through the tunnel URL directly. Don't log in on localhost and then switch to the tunnel URL. The cookies need to be set for the tunnel domain.

Cause 3: Hardcoded localhost URLs

Some apps make API calls to hardcoded URLs like http://localhost:3000/api/data. When accessed through the tunnel, the browser tries to reach localhost on the viewer's machine - which doesn't have your server running.

Fix: Use relative URLs in your API calls (e.g. /api/data instead of http://localhost:3000/api/data). Or use window.location.origin as the base URL so it automatically adapts to whatever domain the app is served on.

Cause 4: WebSocket / SSE Connection Failures

Apps using WebSockets or Server-Sent Events often hardcode the WebSocket URL to ws://localhost:3000. Behind a tunnel, the browser needs to connect to wss://free-abc123.skytunnel.dev instead.

Fix: Derive the WebSocket URL from the current page URL. In JavaScript: const wsUrl = window.location.origin.replace("http", "ws"). This works on both localhost and tunnel URLs.

Cause 5: Mixed Content Blocking

The tunnel URL is HTTPS, but your local app might serve assets or make API calls over HTTP. Browsers block HTTP requests from HTTPS pages (mixed content).

Fix: Make sure your app doesn't hardcode http:// URLs. Use protocol-relative URLs (//example.com) or derive the protocol from the current page.

Let an AI Agent Fix It For You

Don't want to debug manually? Install the skytunnel-fix skill for Claude Code (or any compatible coding agent like OpenCode or Kilocode) and let it diagnose and patch your app automatically.

Install: git clone https://github.com/Pallav0099/skytunnel-fix.git ~/.claude/skills/skytunnel-fix

Then tell your agent: "Fix my tunnel https://free-abc123.skytunnel.dev". It will fetch the tunnel URL, read the error responses, trace them back to CORS, cookie, or hardcoded-URL issues in your codebase, and apply the fixes.

The skill handles all of the causes listed above - CORS/CSRF, cookie domains, hardcoded localhost URLs, WebSocket endpoints, and mixed content - so you can skip the manual steps entirely.

Quick Checklist (Manual)

1. Open browser DevTools (F12) and check the Console and Network tabs for errors.

2. Look for 403 Forbidden - that's CORS/CSRF. Add the tunnel origin to your allowlist.

3. Look for 401 Unauthorized - that's cookies. Log in through the tunnel URL.

4. Look for ERR_CONNECTION_REFUSED - that's hardcoded localhost URLs. Use relative URLs.

5. Look for Mixed Content warnings - switch hardcoded http:// to https:// or relative.

? FAQ

No. These are standard security mechanisms in your app. Any tunnel service (ngrok, Cloudflare, etc.) triggers the same behavior.
Skytunnel itself just forwards traffic, but you can try the skytunnel-fix agent skill — it attempts to diagnose and patch common tunnel issues in your codebase. Results depend on how your app is set up. Install it with: git clone https://github.com/Pallav0099/skytunnel-fix.git ~/.claude/skills/skytunnel-fix
Because localhost and the tunnel URL are different origins. Your app's security checks treat them as separate sites.
Only for development/testing. In production, your app will use its real domain which should already be in the allowlist.