Documentation menu

Exposing localhost to the internet

Give your local development server a public HTTPS URL in about a minute, and inspect every request that arrives through it.

Updated

Your API runs on 127.0.0.1 and something on the internet needs to reach it — a payment provider sending webhooks, a colleague reviewing a feature, a mobile app on real hardware. A tunnel gives that local server a public HTTPS address.

How a tunnel actually works

Worth understanding, because it explains both the security model and why no firewall configuration is needed.

Your machine opens an outbound connection to the tunnel server and keeps it open. The tunnel server owns a public hostname. When a request arrives at that hostname, it is pushed down the connection your machine already opened, handed to your local port, and the response travels back the same way.

Nothing on your network ever listens for inbound connections. That is why tunnels work behind NAT, on hotel wifi, and inside corporate networks that block everything inbound — and it is also why the tunnel server can see the traffic, which is the trade-off you are accepting.

Start the tunnel

  1. Sign in to SchemaClient. Tunneling routes traffic through our servers, so unlike the REST client it needs an account.
  2. Open the Tunnel tab.
  3. Set the target to whatever your dev server is bound to:
http://127.0.0.1:8000
  1. Click Start Tunnel.
  2. Copy the public URL that appears — it looks like https://u1-a3f9.schemaclient.com.

Anything sent to that URL is forwarded to your local port, and the response travels back the same way.

Bind address matters

If your server is bound to 127.0.0.1 it accepts connections only from your own machine, which is fine — the tunnel client runs there too. But if you are running the server inside Docker, 127.0.0.1 inside the container is not your host. Bind to 0.0.0.0 in the container and publish the port, then tunnel the published host port.

Check it works

From any machine, or just a different network:

curl https://u1-a3f9.schemaclient.com/health

If that returns what http://127.0.0.1:8000/health returns locally, the tunnel is up.

When it does not work

SymptomCauseFix
502 or connection refusedLocal server not running, or on a different portConfirm the port your server actually bound to
Works locally, 404 through the tunnelFramework is checking the Host headerAdd the tunnel hostname to allowed hosts
Assets 404, page loads unstyledApp generates absolute http://localhost URLsFind the hardcoded base URL in config
Infinite redirect loopApp forces HTTPS and sees the forwarded request as HTTPTrust X-Forwarded-Proto
Works, then stopsTunnel session endedRestart; the URL may change

The host-header one catches nearly everyone. In Django it is ALLOWED_HOSTS, in Rails config.hosts, in Vite server.allowedHosts. The framework is doing the right thing — rejecting a Host it was not told about — so add the hostname rather than disabling the check.

The redirect loop is the second most common. Your app sees a plain HTTP request from the tunnel and redirects to HTTPS; the tunnel forwards that redirect; the browser comes back; repeat. The fix is to make the app trust the X-Forwarded-Proto header so it knows the original request was already HTTPS.

What does and does not relay

Plain HTTP requests relay, and so do Server-Sent Events — a streaming SSE endpoint on your machine can be consumed through the tunnel, which is useful for testing streaming responses against a hosted client.

Native WebSocket and gRPC connections do not traverse the tunnel. Point those clients directly at your local server. It is worth knowing before you design a demo around it — see WebSocket testing and tunnel streaming for the detail.

Security, briefly

A tunnel is a door you are opening deliberately. Sensible practice:

  • Expose one port, the dev server's — not a database, admin panel or file share.
  • Never tunnel real production data. Use seeded or synthetic data.
  • Treat the URL as a secret while it is live, and share it only with what needs it.
  • Stop the tunnel when the session ends. An idle open tunnel is an idle open door.
  • Keep verifying webhook signatures. The tunnel changes how traffic reaches you, not whether the sender is who they claim to be.

Watch the traffic

The point of tunneling through an API client rather than a bare tunnel is that every request is captured. See monitoring API traffic for filtering and replay, or go straight to testing webhooks locally, which is what most people want a tunnel for in the first place.

Frequently asked questions

Is a localhost tunnel safe to leave running?

Treat the URL as public, because it is. Anyone who has it can reach your local server, so do not tunnel an app holding real production data, and stop the tunnel when you are done testing.

Why do I need an account for tunneling but not for the REST client?

The REST client runs entirely on your machine. A tunnel has to route traffic through our servers to give you a public hostname, so it needs an account to attach the tunnel to.

Do I need to open a port on my router or firewall?

No. The tunnel works by making an outbound connection from your machine to the tunnel server, which then forwards public traffic back down that existing connection. Nothing listens for inbound connections on your network, so it works behind NAT and most corporate firewalls.

Do WebSockets work through the tunnel?

Not currently. HTTP requests and Server-Sent Events relay through the tunnel, but native WebSocket and gRPC connections do not — point those clients directly at your local server instead.