Skip to content

Portless: Named URLs for Local Dev

4 min read

Today I discovered Portless from Vercel Labs, and it fixes a problem I didn’t realize I’d been tolerating for years: port roulette.

You know the drill. Start a dev server on localhost:3000. Start another project. It grabs 3001. Kill a process, restart it, now it’s on 3002. Your bookmarks break. Your cookies bleed between unrelated apps. Your Claude Code session guesses 3000 when the server is actually on 3001.

And then there’s HTTPS. You need it for service workers, OAuth callbacks, secure cookies, WebAuthn. So you install mkcert, generate certs, trust the CA, configure your dev server to load the cert and key files, and hope your teammates did the same thing. Every new project, same ritual.

Portless replaces all of that with stable, named .localhost URLs. HTTPS with HTTP/2 out of the box, no manual cert setup.

How It Works

Portless runs a reverse proxy that binds port 443 with HTTPS and HTTP/2 enabled by default. On first run, it generates a local CA, trusts it (auto-elevates with sudo on macOS/Linux), and issues wildcard *.localhost certificates. When you start a project through it, Portless auto-assigns a free ephemeral port (in the 4000-4999 range), injects it as PORT into your dev server, and maps a named URL to that port.

# Install globally (recommended)
npm install -g portless

# Start your app with a named URL
portless myapp next dev
# → https://myapp.localhost

# Organize services with subdomains
portless api.myapp pnpm start
# → https://api.myapp.localhost

# Or use `run` in package.json (infers name from project)
# "dev": "portless run next dev"

# Alias a fixed-port service like Postgres
portless alias mydb 5432
# → https://mydb.localhost

The browser request to myapp.localhost hits the proxy, which inspects the Host header and forwards to the correct backend port. The mapping persists in ~/.portless/routes.json, so your URLs survive restarts. Configuration (port, TLS, TLD) is remembered across restarts, so a reboot doesn’t silently revert to defaults.

The key insight: the URL is completely decoupled from the actual port. You never need to know or care which port your app landed on.

What It Actually Fixes

ProblemBefore PortlessAfter Portless
Port conflicts (EADDRINUSE)Kill processes manuallyAuto-assigns free port
Cookie/localStorage bleedAll apps share localhostEach app gets its own hostname
Broken bookmarkslocalhost:3000 changes per sessionmyapp.localhost is permanent
No local HTTPSSelf-signed certs, manual trustAuto-generated CA, trusted on first run
Agent port guessingClaude Code tries 3000, 5173, 8080Stable URL in AGENTS.md

Why AI Agents Care

This is the angle that sold me. AI coding agents struggle with ports. Claude Code, Cursor, and similar tools either guess common ports or ask you, which breaks the autonomous flow.

With Portless, you document stable endpoints in your AGENTS.md or project config:

## Dev Services

- Frontend: https://myapp.localhost
- API: https://api.myapp.localhost
- Docs: https://docs.myapp.localhost

The agent can reliably hit the right service every time. No guessing. No “which port is the API on?” interruptions.

Framework Compatibility

Most frameworks (Next.js, Express, Nuxt) respect the PORT environment variable automatically. For those that don’t (Vite, VitePlus, Astro, React Router, Angular, Expo, React Native), Portless auto-injects the right --port and --host flags.

If you need plain HTTP, pass --no-tls. If you need to bypass Portless entirely, PORTLESS=0 pnpm dev runs your normal command with no proxy. In non-interactive environments (no TTY, or CI=1), Portless exits with a descriptive error instead of prompting, so CI scripts fail early with a clear message.

Git Worktree Integration

If you already use git worktrees, Portless detects them automatically. In a linked worktree, the branch name is prepended as a subdomain so each worktree gets its own URL with zero config:

# Main worktree
portless run next dev   # → https://myapp.localhost

# Linked worktree on branch "fix-ui"
portless run next dev   # → https://fix-ui.myapp.localhost

Put portless run in your package.json once and it works everywhere. Main checkout uses the plain name, each worktree gets a unique subdomain. No collisions, no --force.

What I Learned

  • Portless binds port 443 with HTTPS and HTTP/2 by default, generating and trusting a local CA on first run
  • Each app gets its own hostname (myapp.localhost), eliminating cookie bleed and port conflicts
  • Clean URLs with no port numbers: https://myapp.localhost instead of http://localhost:3000
  • The real win is for AI agents: stable, documentable URLs mean agents stop guessing ports
  • Framework detection auto-injects the right flags for Vite, Astro, and others that ignore PORT
  • --no-tls for plain HTTP, PORTLESS=0 to bypass entirely, and auto-exit in CI environments

References


Rethinking your local dev setup for AI agents? I’d love to hear what tools are in your stack. Reach out on LinkedIn.