Portless: Stable .localhost URLs for Agent Workflows
Local development usually means localhost:3000, localhost:8080, and a mental map of which port belongs to which service. AI agents struggle with this. They cannot reliably track dynamic port assignments across restarts, and they cannot predict which port a framework will choose when you spin up a ne
Local development usually means localhost:3000, localhost:8080, and a mental map of which port belongs to which service. AI agents struggle with this. They cannot reliably track dynamic port assignments across restarts, and they cannot predict which port a framework will choose when you spin up a new service. Portless replaces port numbers with stable .localhost URLs. Instead of http://localhost:3000, you get https://myapp.localhost. The tool handles TLS, port assignment, and framework-specific CLI flag injection so your services bind to the right port without manual configuration. It hit 12,000 GitHub stars in days and ranked #3 on TypeScript trending. The reason is simple: it solves a coordination problem that gets worse as you add more services or more agents to your workflow. Agents need predictable endpoints. When you ask an agent to "start the frontend and call the API," it needs to know where the API will be. If the API binds to a random port, the agent must parse logs, guess, or fail. Human developers memorize port numbers or check package.json. Agents do not have that context unless you hard-code it into every prompt or tool definition. Portless removes the guesswork by giving every service a stable name. The second problem is multi-service orchestration. If you run three services locally, you need three terminal tabs, three port numbers, and a mental model of which service depends on which. Portless does not solve the orchestration part, but it does solve the naming part. https://api.localhost, https://frontend.localhost, and https://worker.localhost are easier to wire together than localhost:4001, localhost:4002, and localhost:4003. Portless is a reverse proxy that binds port 443 and routes requests to your services based on hostname. The flow looks like this: You run portless myapp next dev. Portless assigns a random port (4000-4999) and sets the PORT environment variable. Portless starts a proxy on port 443 (auto-elevates with sudo on macOS/Linux). Portless generates a local CA and trusts it in your system keychain (first run only). Your framework starts on the assigned port. Requests to https://myapp.localhost hit the proxy, which forwards them to localhost:4xxx. The proxy auto-starts when you run an app. It reuses configuration (port, TLS, TLDs) from the most recent run, so a reboot does not silently revert to defaults. This means the proxy remembers your settings, but you need to re-run portless trust if the state directory format changes between versions (pre-1.0 warning). Most frameworks respect the PORT environment variable. Next.js, Express, and Nuxt bind to process.env.PORT automatically. Vite, Astro, Angular, Expo, and React Native do not. Portless detects these frameworks and injects the right --port flag (and --host when needed). The detection logic works like this: Portless parses your package.json script. If the script starts with a known framework name (vite, astro, ng, expo) or a runner prefix (bunx vite, npx astro), Portless classifies it. If the script is a server command (dev, serve, preview, start), Portless injects --port and --host. If the script is a build command (vite build, astro check), Portless leaves it alone. Portless also leaves scripts alone when injection would break them: Compound commands (&&, |, ;) Trailing # comments Env prefixes (NODE_ENV=production vite) Delegation to another script ("dev": "npm run dev:vite") Runner flags before the script name (bun run --bun dev) If Portless cannot classify a script, it does not inject flags. You set the port yourself in those cases. Portless stores its state in a directory (location varies by OS). This includes: The local CA certificate Proxy configuration (port, TLS, TLDs) Trusted certificate metadata The state directory format may change between releases. If you install Portless globally, this is not a problem. If you install it per-project, different contributors may run different versions. When the format changes, you need to re-run portless trust to regenerate the CA and update the state directory. This is a coordination risk for teams. If one contributor upgrades Portless and the state format changes, other contributors see certificate errors until they upgrade and re-run portless trust. The recommended approach is global install to avoid version drift. Portless generates a local CA and trusts it in your system keychain. This CA signs certificates for .localhost domains. The CA private key lives in the state directory. If an attacker gets the key, they can sign certificates for any .localhost domain on your machine. The risk is local. The CA is not trusted outside your machine, so an attacker cannot use it to impersonate public domains. But they can impersonate local services, which matters if you run untrusted code locally. Portless auto-elevates with sudo to bind port 443. This happens once per proxy start. The proxy itself does not run as root, only the bind operation. If you use --no-tls, Portless binds port 80 instead, which also requires elevation on most systems. Portless does not log requests by default. You see framework logs (Next.js, Vite, etc.), but not proxy logs. If a request fails, you need to check the framework logs to see why. Common failure modes: Failure Cause Fix Certificate error State directory format changed Re-run portless trust Port already in use Another process bound port 443 Stop the other process or use --no-tls Framework ignores PORT Framework not in detection list Set port manually in script Proxy does not start Sudo elevation failed Check system permissions Service unreachable Framework bound to 127.0.0.1 instead of 0.0.0.0 Portless injects --host for known frameworks The proxy auto-start behavior can hide failures. If the proxy fails to start, Portless does not block your framework from starting. Your service runs on the assigned port, but https://myapp.localhost does not resolve. You need to check the terminal output to see if the proxy started. Portless is local-only. It does not deploy to production. It does not configure DNS. It does not manage cloud infrastructure. It solves the local development naming problem. If you want stable URLs in production, you need a real reverse proxy (nginx, Caddy, Traefik) and real DNS. Portless is not a replacement for that. It is a replacement for remembering which port your API is on. The global install pattern means Portless is a system-level tool, not a project dependency. This is unusual for Node.js tooling, but it makes sense for a tool that binds privileged ports and manages system certificates. You do not want different versions of Portless fighting over port 443. # Terminal 1: Start the API portless api express src/server.js # -> https://api.localhost # Terminal 2: Start the frontend portless frontend next dev # -> https://frontend.localhost # Terminal 3: Start the worker portless worker node src/worker.js # -> https://worker.localhost Your frontend can now call https://api.localhost/users without hard-coding a port. Your agent can do the same. The URLs are stable across restarts. If you need to pass the API URL to your frontend, set it in .env.local: NEXT_PUBLIC_API_URL=https://api.localhost No port numbers. No environment-specific logic. The same URL works for every developer and every agent. Use Portless when: You run multiple local services and need stable URLs for inter-service calls. You work with AI agents that need predictable endpoints. You want HTTPS locally without manual certificate setup. You can install tools globally and coordinate versions across your team. Avoid Portless when: You run a single service and do not care about the port number. You cannot install global tools (locked-down corporate environments). You need per-project version pinning for reproducibility. You use frameworks with complex CLI flag grammars that Portless does not recognize. Portless is pre-1.0. The state directory format may change. If you install per-project, expect coordination overhead when versions diverge. If you install globally, expect to re-run portless trust after upgrades. The tool does one thing well: it replaces port numbers with names. It does not orchestrate services, manage environment variables, or handle deployment. If you need those, you still need Docker Compose, Tilt, or a task runner. But if you just need stable local URLs, Portless is the simplest option. Portless GitHub Repository
Key Takeaways
- โขLocal development usually means localhost:3000, localhost:8080, and a mental map of which port belongs to which service
- โขThis story was reported by Dev.to, covering developments in the dev space.
- โขAI advancements continue to reshape industries โ read the full article on Dev.to for complete coverage.
๐ Continue reading the full article:
Read Full Article on Dev.to โShare this article



