You've claimed a stable port. Your dev server is running. Now you need someone else to see it -- a client across town, a webhook from Stripe, your phone on the couch. Port Daddy's tunnel integration makes that a single command.
Why You Need Tunnels
Every developer hits these moments:
- Client review at 11pm -- "Can you show me where we're at?" You need a URL, not a deploy pipeline.
- Webhook testing -- Stripe, GitHub, Twilio all need a public URL to POST to. Localhost won't cut it.
- Mobile testing -- Your phone can't reach
localhost:3100. A tunnel gives it a real HTTPS URL. - Cross-device testing -- Test on iPad, Android tablet, your partner's laptop. One URL, all devices.
- Cross-team integration -- Backend team in another timezone needs to hit your local API while you sleep.
Without Port Daddy, you'd juggle separate tunnel CLIs, remember which port maps to which tunnel, and manually restart tunnels when your dev server bounces. Port Daddy unifies all of it.
How It Works
Port Daddy doesn't reinvent tunneling. It wraps the three most popular providers and manages the lifecycle for you:
| Provider | Best For | Auth Required |
|---|---|---|
| ngrok | Production-quality tunnels, custom domains, dashboard | Free account (optional for basic use) |
| cloudflared | Zero-config, no signup, fast Cloudflare network | None |
| localtunnel | Quick and dirty, npm-native, no binary install | None |
When you run pd tunnel start, Port Daddy:
- Looks up the service's port from your claim
- Auto-detects which providers are installed on your machine
- Starts the tunnel process and captures the public URL
- Stores the URL alongside the service in the registry
- Monitors the tunnel process and logs activity
Installing a Tunnel Provider
First, check what you already have:
$ pd tunnel providers
ngrok installed v3.6.0
cloudflared installed 2024.2.1
localtunnel not found
If nothing is installed, pick one:
ngrok (Recommended)
The gold standard. Stable URLs, a web dashboard at localhost:4040, request inspection, and replay.
# macOS
$ brew install ngrok
# npm (cross-platform)
$ npm install -g ngrok
# Optional: authenticate for stable URLs
$ ngrok config add-authtoken YOUR_TOKEN
cloudflared
Zero signup, zero config. Cloudflare's network is fast, and the URLs are temporary by default.
# macOS
$ brew install cloudflare/cloudflare/cloudflared
# Linux
$ curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 \
-o /usr/local/bin/cloudflared
$ chmod +x /usr/local/bin/cloudflared
localtunnel
The npm-native option. No binary to install, just a package.
$ npm install -g localtunnel
Localtunnel is the simplest to set up, but URLs change on every restart and it can be less reliable under load.
Starting a Tunnel
You need a claimed port first. Tunnels attach to existing services:
# Step 1: Claim a port and start your dev server
$ pd claim myapp:frontend
Port 3100 assigned to myapp:frontend
$ PORT=$(pd claim myapp:frontend -q) npm run dev
# Step 2: Open a tunnel (in another terminal)
$ pd tunnel start myapp:frontend
Tunnel started for myapp:frontend
Provider: ngrok
Public URL: https://abc123.ngrok-free.app
Local port: 3100
That's it. The URL is live. Send it to your client, paste it into a Stripe webhook config, or open it on your phone.
Want to use a specific provider?
# Force cloudflared instead of the default
$ pd tunnel start myapp:frontend --provider cloudflared
Tunnel started for myapp:frontend
Provider: cloudflared
Public URL: https://random-words-here.trycloudflare.com
Local port: 3100
Managing Active Tunnels
See all active tunnels at a glance:
$ pd tunnel list
SERVICE PROVIDER PORT URL
myapp:frontend ngrok 3100 https://abc123.ngrok-free.app
myapp:api cloudflared 3101 https://random-words.trycloudflare.com
Check a specific tunnel's status:
$ pd tunnel status myapp:frontend
Service: myapp:frontend
Provider: ngrok
Port: 3100
URL: https://abc123.ngrok-free.app
Status: active
Started: 2 minutes ago
Stop a tunnel when you're done sharing:
$ pd tunnel stop myapp:frontend
Tunnel stopped for myapp:frontend
Real Scenarios
Scenario 1: Show the Client at 11pm
Your client texts: "Can I see the new checkout flow?" You're in pajamas. No problem.
# Your dev server is already running on its stable port
$ pd tunnel start myapp:frontend
Public URL: https://abc123.ngrok-free.app
# Text the URL to your client. Done.
# They see your live local dev server over HTTPS.
No deploy. No staging environment. No waiting for CI. Your laptop becomes a server for exactly as long as you need it.
Scenario 2: Webhook Debugging with Stripe
Stripe needs a public URL for webhook events. Port Daddy makes this seamless:
# Start your API and expose it
$ pd claim myapp:api
Port 3101 assigned to myapp:api
$ pd tunnel start myapp:api
Public URL: https://def456.ngrok-free.app
# Now configure Stripe's webhook endpoint:
# https://def456.ngrok-free.app/webhooks/stripe
# Every webhook POST from Stripe hits your local machine.
# Set breakpoints, inspect payloads, debug in real time.
If you're using ngrok, open localhost:4040 to inspect every request and response in detail. Replay failed webhooks with a single click.
Scenario 3: Mobile Testing
Your responsive layout looks perfect in Chrome DevTools. Does it actually work on a real phone?
$ pd tunnel start myapp:frontend
Public URL: https://abc123.ngrok-free.app
# Open on your phone's browser. Test touch events,
# viewport quirks, and actual mobile performance.
# Changes in your editor appear live on the phone.
Hot reload works through the tunnel. Edit a component, save, and watch it update on your phone in real time.
Scenario 4: Cross-Team Integration
The backend team needs to test against your frontend, but they're in a different timezone:
# Expose both your frontend and API
$ pd tunnel start myapp:frontend
Public URL: https://abc123.ngrok-free.app
$ pd tunnel start myapp:api
Public URL: https://def456.ngrok-free.app
# Share both URLs in Slack.
# They hit your local services like they're deployed APIs.
JavaScript SDK Integration
Manage tunnels programmatically from your Node.js scripts, test harnesses, or agent workflows:
import { PortDaddy } from 'port-daddy/client';
const pd = new PortDaddy();
// Check which providers are available
const providers = await pd.tunnelProviders();
console.log(providers);
// { ngrok: true, cloudflared: true, localtunnel: false }
// Start a tunnel for a claimed service
const tunnel = await pd.tunnelStart('myapp:frontend', 'ngrok');
console.log(tunnel.url);
// https://abc123.ngrok-free.app
// Check tunnel status
const status = await pd.tunnelStatus('myapp:frontend');
console.log(status.status);
// "active"
// List all active tunnels
const list = await pd.tunnelList();
for (const t of list.tunnels) {
console.log(`${t.serviceId} -> ${t.url}`);
}
// Stop a tunnel
await pd.tunnelStop('myapp:frontend');
Automated Test Setup
Use tunnels in your E2E test harness to test against real webhook providers:
import { PortDaddy } from 'port-daddy/client';
import { test, beforeAll, afterAll } from 'vitest';
let pd;
let tunnelUrl;
beforeAll(async () => {
pd = new PortDaddy();
// Claim port and start tunnel
const claim = await pd.claim('test:webhook-server');
const tunnel = await pd.tunnelStart('test:webhook-server');
tunnelUrl = tunnel.url;
// Register the tunnel URL with Stripe for testing
await stripe.webhookEndpoints.create({
url: `${tunnelUrl}/webhooks/stripe`,
enabled_events: ['payment_intent.succeeded'],
});
});
afterAll(async () => {
await pd.tunnelStop('test:webhook-server');
await pd.release('test:webhook-server');
});
test('receives Stripe webhook', async () => {
// Trigger a real Stripe test payment
// Your local server receives the webhook through the tunnel
});
Security Considerations
Tunnels make your local machine reachable from the public internet. That's powerful and dangerous in equal measure. Keep these rules in mind:
1. Public URLs Are Public
Anyone with the URL can reach your dev server. Don't leave tunnels running overnight unless you intend to. Stop them when you're done:
$ pd tunnel stop myapp:frontend
2. Never Expose Credentials
If your dev server has an admin panel without auth, a debug endpoint that dumps env vars, or a database UI -- those are all reachable through the tunnel. Review what your dev server exposes before opening it up.
3. Audit Tunnel Activity
Port Daddy logs all tunnel start/stop events in the activity log:
$ pd log --type tunnel
2026-03-01T23:14:00Z tunnel.start myapp:frontend ngrok https://abc123.ngrok-free.app
2026-03-01T23:47:00Z tunnel.stop myapp:frontend
4. Use ngrok's Built-In Protection
ngrok supports basic auth, IP allowlists, and OAuth on its paid tiers. If you're exposing anything sensitive, consider adding a layer:
# ngrok's native auth (outside Port Daddy)
$ ngrok http 3100 --basic-auth "user:password"
5. Rate Limiting
Port Daddy's built-in rate limiting (100 requests/minute per IP) applies to the daemon API, not to your tunneled service. Your dev server is responsible for its own rate limiting when exposed publicly.
Troubleshooting
"Tunnel won't start"
Most common cause: the service isn't claimed yet. Tunnels attach to existing services.
# Check if the service exists
$ pd find myapp:frontend
myapp:frontend port=3100 claimed
# If not found, claim it first
$ pd claim myapp:frontend
# Check if any provider is installed
$ pd tunnel providers
ngrok not found
cloudflared not found
localtunnel not found
# Install one (ngrok recommended)
$ brew install ngrok
"502 Bad Gateway"
The tunnel is running, but your dev server isn't. The tunnel forwards to the port, but nothing is listening there.
# Verify your dev server is actually running
$ curl http://localhost:3100
curl: (7) Failed to connect to localhost port 3100: Connection refused
# Start your dev server first, then try the tunnel URL again
$ PORT=$(pd claim myapp:frontend -q) npm run dev
"Webhooks don't arrive"
Three things to check:
- URL is correct -- Copy the full tunnel URL including the path (
https://abc123.ngrok-free.app/webhooks/stripe, not just the domain). - Tunnel is still active -- Run
pd tunnel status myapp:apito confirm it hasn't died. - Firewall rules -- Some corporate networks block tunnel providers. Try switching providers:
$ pd tunnel stop myapp:api $ pd tunnel start myapp:api --provider cloudflared
"URL changed after restart"
Free-tier ngrok and localtunnel generate random URLs on each start. This is expected. If you need stable URLs:
- ngrok -- Paid plans offer reserved domains (
your-name.ngrok.io) - cloudflared -- Set up a Cloudflare Tunnel with a fixed subdomain
- Workaround -- Script your webhook provider to update the URL on tunnel start
What's Next
You can now expose any local service to the world in one command. Here's where to go from here:
- Monorepo Mastery -- Manage 50 services with
pd scanandpd up - Debugging -- Diagnose phantom port conflicts and zombie processes
- Full API Reference -- Every endpoint, flag, and SDK method
The tunnel is the bridge between "it works on my machine" and "here, see for yourself." Use it generously.