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:
# First run reserves the name; later runs reuse it
npx otterkit tunnel 3000 --subdomain my-api
# -> https://my-api.otterkit.app, every timeKill 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:
npx otterkit tunnel 5173 --ttl 4h # an afternoon demo, then it stops itself
npx otterkit tunnel 3000 --ttl 45m # a quick review callBasic 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:
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:
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 requestThat 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:
# 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"npx otterkit up # Start everything
npx otterkit down # Stop the daemons started from the configup 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.