Portless: named .localhost URLs for local dev, with a proxy that learns your framework
Replace port numbers with stable, named local URLs. For humans and agents.
At a glance
- What is it?
- Portless replaces port numbers with stable, named .localhost URLs and manages a local HTTPS proxy and CA. It targets developers and agents who want predictable local endpoints, with automatic flag injection for frameworks that ignore PORT.
- Who is it for?
- Adopt portless if you run many local services and want stable, readable URLs like https://myapp.localhost without remembering ports, especially in monorepos or with agents that need deterministic endpoints. Skip it if you prefer explicit control over every process flag or if your scripts use compound commands, env prefixes, or delegated scripts that portless deliberately leaves alone.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 1 day ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 15, 2026, and from our analysis. They are not legal advice.
DEEP OPEN-SOURCE ANALYSIS
What portless actually replaces
Local development usually means remembering which service runs on which port. Portless replaces that with stable, named .localhost URLs. Instead of http://localhost:3000 you get https://myapp.localhost. The README frames it as a tool for humans and agents, which is a useful distinction. Humans stop guessing ports; agents get a fixed URL to call. The core mechanism is a proxy that binds port 443, generates a local CA on first run, trusts it, and routes requests to child processes on random ports in the 4000-4999 range. The child process receives its port via the PORT environment variable, which most frameworks respect. The proxy auto-starts when you run an app, and it reuses the configuration from the most recent run, so a reboot does not silently revert to defaults. Explicit env vars like PORTLESS_PORT and PORTLESS_HTTPS always take priority over the reused config.
How the proxy and CA trust flow works
On first run, portless generates a local CA, trusts it, and binds port 443. On macOS and Linux it auto-elevates with sudo to bind that privileged port. The state lives in ~/.portless, and when the proxy runs under sudo, portless resolves that path from the invoking user's home, so the privileged proxy and the unprivileged app processes share the same route registrations. This is a deliberate design choice to avoid a split-brain setup where the proxy and the app disagree about which hostname maps to which process. The README warns that the state directory format may change between pre-1.0 releases, which can require re-running portless trust. That is a concrete maintenance cost: an upgrade might invalidate your CA trust and force a manual step. The trust flow is not a one-time setup if you update frequently.
Flag injection: the part that is easy to get wrong
Not every framework honors the PORT environment variable. The README lists Vite, VitePlus, Astro, React Router, Angular, Expo, and React Native as frameworks that ignore it. For those, portless auto-injects the right --port flag and, when needed, a matching --host flag. The injection only reaches through a package script whose command starts with the framework or a known runner, like "dev": "vite" or "dev": "bunx vite". It only touches server commands: dev, serve, preview, start, a bare vite, or vite [root]. A command that does not serve, such as vite build, vite optimize, vp test, or astro check, rejects the flags and is left alone. This classification is the most fragile part of the tool. The README gives examples of scripts it deliberately does not modify: a compound command with &&, |, or ;, a trailing # comment, its own -- option terminator, an env prefix like NODE_ENV=production vite, delegation to another script like npm run dev:vite, or runner flags before the script name like bun run --bun dev. In those cases, portless leaves the script alone and you must set the port yourself. That is a real limitation, not a hypothetical one. If your dev script uses any of those constructs, portless will not help you, and you will need to restructure the script or accept a random port.
Configuration: from bare command to monorepo maps
Bare portless runs the dev script from package.json, inferring the app name from the package name, git root, or directory. An optional portless.json can override the name, script, app port, proxy on/off, and more. The config fields are name, script, appPort, proxy, apps, and turbo. The apps field is for monorepos: one portless.json at the repo root can cover all workspace packages, keyed by relative path. Hostnames follow the convention <package>.<project>.localhost, where the project name comes from the most common npm scope across workspace packages, falling back to the workspace root directory name. If a package's short name matches the project name, it gets the bare <project>.localhost without duplication. You can also put a "portless" key in package.json, which takes precedence over portless.json app entries but is overridden by CLI flags. The --script flag overrides the default script for a single invocation, so you can run portless --script start or portless --script test.
Running it: commands and the turborepo path
Installation is a global npm install -g portless or a project dev dependency. The README recommends global install because pre-1.0 per-project installs can mean different contributors run different versions. To run an app, you write portless myapp next dev, and you get https://myapp.localhost. Use --no-tls for plain HTTP. You can also put portless in a package.json script: "dev": "portless run next dev". With a portless.json, you can simplify the script to just "dev": "next dev" and run portless or portless run. For turborepo, the README shows a pattern: put portless as the dev script and the real command in a separate script, like "dev": "portless" and "dev:app": "next dev", with a portless config pointing script to dev:app. Portless detects the package manager and runs pnpm run dev:app (or yarn/bun/npm) through the proxy. No changes to turbo.json are needed. People without portless can still run pnpm run dev:app directly. This is a clean escape hatch, but it means your package.json carries an extra indirection layer.
Non-interactive behavior and a real alternative
In non-interactive environments, such as no TTY or CI=1, portless exits with a descriptive error instead of prompting. That is good for task runners like turborepo and CI scripts, which fail early with a clear message. The alternative is to keep using plain localhost with fixed ports, which is what most projects do today. The difference is that with plain localhost you control every flag yourself, and you do not need a proxy or a CA trust step. Tools like mkcert can provide the CA trust part, but they do not give you named hostnames or port assignment. Another alternative is to use a reverse proxy like Caddy or nginx with a hosts file entry, which gives you named URLs but requires manual configuration for each service. Portless automates the proxy and the naming, but it adds a layer of abstraction that can hide what is actually running. If you need to debug a network issue, you have to reason about the proxy's routing rather than a simple port.
Maintenance cost and license implications
The README is explicit that portless is pre-1.0. The state directory format may change between releases, which can require re-running portless trust after an upgrade. That is a concrete maintenance cost that you should budget for. The project is under active development, with releases at v0.15.6, v0.15.5, and v0.15.4 in recent months, so changes are coming. The license is Apache-2.0, which is permissive for commercial use, but it does not come with any warranty or support guarantee. You are adopting a tool that may change its internal format, and you will need to watch release notes for breaking changes. The README also warns that when installed per-project, different contributors may run different versions, which can lead to inconsistent behavior across a team. That is a real coordination cost, and it argues for the global install path if you adopt it.
Editorial conclusion
Adopt portless if you run many local services and want stable, readable URLs like https://myapp.localhost without remembering ports, especially in monorepos or with agents that need deterministic endpoints. Skip it if you prefer explicit control over every process flag or if your scripts use compound commands, env prefixes, or delegated scripts that portless deliberately leaves alone. Before adopting, verify that your framework's server command is classified as serving (not build or check) and that your script does not contain constructs portless refuses to modify; check the current state directory format, since pre-1.0 releases may require re-running portless trust after upgrades.
Community notes