All articles
TunnelsAugust 13, 2026 · 8 min read

Share localhost with a public URL in one command

Put an HTTPS URL on any local port: demo work-in-progress to a client, test on a real phone, or receive provider callbacks - with a subdomain that stays stable across restarts.


"Can you send me a link?" is the most expensive sentence in a code review. The work is running on your machine, finished enough to look at - and the honest answers are all bad: deploy to staging (twenty minutes, plus whatever staging is currently broken by), screen-share it (nobody can click anything), or screenshot it (now you're reviewing a painting).

The missing move is a tunnel: a public HTTPS URL that forwards straight into the dev server you already have running. One command, and your localhost is a link.

One command

$ npx otterkit tunnel 3000
Checking login…
Provisioning tunnel...
Tunnel provisioned: tunnel-a1b2c3d4

  Tunnel ready: https://tunnel-a1b2c3d4.otterkit.app -> 127.0.0.1:3000

  Press Ctrl+C to disconnect

  200 GET / (12ms)
  200 GET /api/users (8ms)

That URL is HTTPS with a real certificate, reachable from anywhere, and every request streams into your terminal as it happens. Send it to the client, open it on your phone, paste it into the OAuth console - it's just a URL. Ctrl+C and it's gone.

A URL that survives restarts

Random names are fine for a one-off. The moment a URL is written down somewhere - a provider's webhook config, an OAuth redirect URI, a teammate's bookmark - it needs to stop changing. --subdomain reserves a name to your workspace:

bash
# First run reserves the name; later runs reuse it
npx otterkit tunnel 3000 --subdomain my-api
# -> https://my-api.otterkit.app, every time

Kill the tunnel, reboot, come back tomorrow - the URL is the same. You can hold up to 10 reserved names per workspace, and holding them costs nothing.

Guardrails for real-world sharing

A public URL into your laptop deserves two safety habits, and both are flags. A TTL stops the tunnel server-side when the timer runs out - the fix for the tunnel you forgot about at 6pm:

bash
npx otterkit tunnel 5173 --ttl 4h     # an afternoon demo, then it stops itself
npx otterkit tunnel 3000 --ttl 45m    # a quick review call

Basic auth keeps the URL private to the people you gave credentials. The check runs in the CLI on your machine - requests without valid credentials get a 401 and are never forwarded, and the credentials are never sent to or stored by OtterKit's servers:

bash
npx otterkit tunnel 3000 --auth admin:s3cret
curl -u admin:s3cret https://tunnel-a1b2c3d4.otterkit.app/

See exactly what came in

Run with --log and every request - headers, body, status, duration - is captured to a local JSONL log you can browse, filter, and replay:

bash
npx otterkit tunnel 3000 --subdomain my-api --log

npx otterkit inspect my-api --follow              # tail live
npx otterkit inspect my-api --status 5xx          # just the failures
npx otterkit replay my-api                        # re-send the last request

That turns "the client says it broke on their machine" from a rumor into a capture: the exact request, the exact response, replayable against your fix.

The phone test, without the ritual

Mobile Safari bugs don't reproduce in a resized desktop window. With a tunnel up, the real device test is: open the URL on the phone. No same-WiFi requirement, no USB debugging, no adb reverse - and because the URL is HTTPS, everything that requires a secure context (camera, geolocation, service workers) works like production.

Whole projects, one command

Real work is rarely one port. Define the project's tunnels once in otterkit.toml, then bring the whole environment up and down like a stack:

toml
# otterkit.toml
[tunnels.web]
port = 3000
subdomain = "myapp"          # stable URL (optional)
ttl = "8h"                   # auto-stop (optional, default 24h)

[tunnels.hooks]
webhook = true               # capture-only endpoint, no local server
subdomain = "myapp-hooks"
bash
npx otterkit up          # Start everything
npx otterkit down        # Stop the daemons started from the config

up is idempotent - already-running profiles are skipped - so it's safe in setup scripts, Makefiles, and agent loops. Check otterkit.toml into the repo and "get a shareable environment" becomes part of the project, not tribal knowledge.

Pricing is metered and boring on purpose: 1 credit ($0.01) per connected hour, billing pauses while disconnected, and no tunnel ever bills more than $3 per rolling 30 days. The 10 free signup credits are about 10 tunnel hours - no card required. More in the tunnels docs.