How Portless Works
myapp.localhost instead of localhost:3000 โ a proxy that replaces port numbers with names
Local development means port hell: localhost:3000, localhost:3001, localhost:5173... You can't remember which is which, and telling an AI agent 'connect to port 3000' breaks the next time it changes.
Portless solves this fundamentally. Run portless myapp next dev and access it at http://myapp.localhost.
Core Architecture: Name โ Port Mapping Proxy
Browser Portless Proxy (443) Dev Server
myapp.localhost โโโโโโโ routes.json lookup โโโโโโโ localhost:4231
api.myapp.localhost โโโ subdomain match โโโโโโโ localhost:4582
Execution Flow: portless myapp next dev
Proxy daemon check โ reads
~/.portless/proxy.pid. If not running, auto-starts as detached child process. Not a system service โ starts on first use.Port assignment โ random port in 4000โ4999 range. Tests with
net.createServer().listen().Framework detection + port injection
Next.js, Express, Remix โ honor
PORTenv var nativelyVite, Astro, Angular โ don't honor
PORT. CLI flags--port,--hostauto-injected.
Route registration โ writes
{ hostname: 'myapp', port: 4231, pid: 12345 }toroutes.jsonChild process spawn โ runs with
PORT=4231. Route auto-removed on exit.
Proxy Internals
Peeks first byte of TCP socket to branch TLS vs plaintext HTTP on a single port (443).
Routing: exact hostname match first, then subdomain match. Loop detection via x-portless-hops header (508 at 5+).
Why .localhost Works
.localhost is RFC 2606 reserved. Chrome/Firefox/Edge resolve *.localhost to 127.0.0.1 without /etc/hosts. Safari needs hosts file sync (portless handles this).
Auto HTTPS
- Self-signed CA (EC, 10-year validity)
- SNI callback generates per-hostname certs dynamically
portless trustinstalls CA in system keychainNODE_EXTRA_CA_CERTSinjected to child processes
File-Based State
routes.json re-read on every request (no cache). Directory-creation mutex for locking. Stale routes auto-GC'd by PID liveness check.
Worktree Support (v0.5.2+)
Detects git worktree branch, prepends to hostname: feat-auth.myapp.localhost
How It Works
CLI checks proxy daemon โ auto-starts as detached process if absent (port 443)
Assign free port in 4000โ4999 + inject port per framework (PORT env or --port flag)
Register { hostname, port, pid } in routes.json โ proxy reads this file on every request for routing
TCP first-byte peek (0x16 = TLS) branches HTTPS/H2 vs plain HTTP โ single port handles both
SNI callback dynamically generates per-hostname certs + caching. Self-signed CA registered in system trust store
Auto-remove from routes.json on child exit + zombie route GC via PID liveness check
Pros
- ✓ No need to remember port numbers โ name-based URLs
- ✓ Auto HTTPS + HTTP/2 โ cert generation and install in one step
- ✓ Framework-agnostic โ supports Next.js, Vite, Express, Nuxt and more
- ✓ Not always-on โ auto-starts on first use, no system service registration
- ✓ Worktree + subdomain support for branch-independent access
Cons
- ✗ Port 443 requires sudo on macOS/Linux
- ✗ Safari doesn't auto-resolve *.localhost โ /etc/hosts sync needed
- ✗ routes.json read on every request โ possible I/O overhead under extreme request rates
- ✗ Additional configuration needed inside Docker containers (force PID)
- ✗ Requires Node.js 20+, no Windows support