We use cookies to enhance your experience on the site
CodeWorlds
Back to collections
Guide13 min read

Portless, or names instead of port numbers

Portless replaces port numbers with named .localhost addresses, with HTTPS and separate cookies. Installation, monorepo work, sharing, and the limitations.

Portless, or names instead of port numbers

Portless replaces addresses like localhost:3000 with named addresses such as shop.localhost, served over HTTPS. Instead of remembering which project claimed which number, you start a server under a name and open it under that name.

The tool comes from Vercel's labs, is open source, and works independently of any framework. You run it before any command that starts a development server, and a proxy running in the background handles the rest.

The problem it solves

Three annoyances of local work that seem minor separately and together eat a surprising amount of time.

The first is number collisions. The fifth service you start reports the port as taken, so you change the number, and the next day you cannot remember which project runs where. A tab opened yesterday shows an entirely different application today.

The second is browser data bleeding across projects. Everything on localhost shares the same cookie and local storage space, so a session in one project affects another. Logging out in one place can log you out of three others.

The third is the absence of encryption. Some browser features work only over an encrypted connection, so a local environment without one behaves differently from production, and the difference usually surfaces the day after a deploy.

Named addresses solve all three at once, since a separate name is a separate origin as far as the browser is concerned, and the proxy handles encryption.

Getting started

Code
Bash
npm install -g portless
portless shop next dev

The server runs as before, on an assigned number, while you open https://shop.localhost. The proxy forwards traffic, so the framework needs no configuration and does not even need to know something sits in front of it.

The certificate for the encrypted connection is created on first run, along with a local certificate authority added to the system trust store. That needs administrator privileges once, at first launch, and then works without asking.

Install the proxy as a system service right away.

Code
Bash
portless service install

Without that step, you must start it manually after every reboot, which quickly undoes the convenience. The service starts with the system, and removing it is equally one command.

Working in a monorepo

This is where the tool shows its biggest advantage, since a repository holding six applications means six numbers to remember.

Code
Bash
portless web next dev
portless api node server.js
portless docs vitepress dev
portless panel vite

The addresses become web.localhost, api.localhost, docs.localhost, and panel.localhost, each stable across sessions. A bookmark saved in the browser always leads where it led yesterday.

Subdomains work too, so api.web.localhost beside web.localhost lets you mirror the arrangement from production. That matters when testing cookie rules across subdomains, since locally they then behave as they will on the real domain.

Code changes required are usually none or minor. Addresses hardcoded in configuration need replacing, and exposing them through environment variables is the simplest route, which is better practice anyway.

Three places are worth checking while you are at it: the cross origin access rules on the server side, the callback addresses for sign in through an external provider, and the base address in tests that drive a browser. All three usually hold an address with a port number typed in quickly once and forgotten, and each of them fails in a different, confusing way.

Sharing beyond your own machine

Beyond local names, the tool covers three scenarios where an address should be visible more widely.

The first is the local network. A name in the appropriate domain lets a phone on the same network open the application without hunting for the computer's IP address. When testing a mobile view that saves a few minutes per session.

The second is sharing over a private overlay network, enabled with one flag. The application becomes reachable for devices enrolled in the same network, with nothing exposed to the public internet.

The third is a public address, and here the tool does not send you off to a separate program. One flag exposes the application through the public branch of that same overlay network, another through an external tunnelling service. In both cases the local address keeps working, and the tunnel is cleaned up when the application exits.

Each of these modes is one switch, on starting the application and on starting the proxy itself respectively.

Code
Bash
portless myapp --tailscale next dev
portless myapp --funnel next dev
portless myapp --ngrok next dev
portless proxy start --lan

Each has an environment variable equivalent, which is often more convenient for team work: written once into an environment file it removes the need to remember the flag on every start and does not accidentally end up in a command committed to the repository.

Distinguishing the second mode from the third matters most here, since it decides who sees your application. The overlay network flag on its own keeps traffic between your own devices, so it suits showing work to a team and will not hand an address to a client outside the organisation. Only the public flag or the external tunnel flag opens the address to anyone who receives it, and both require the matching service to be enabled on the vendor's side beforehand, otherwise the command fails before the server even starts.

What changes in daily work

Three things that after a week of use turn out to matter more than the convenience of remembering names.

The first is bookmarks and browser history. An address stable across sessions makes address bar autocomplete finally work properly, instead of suggesting four different projects under one number. Working across several repositories at once, that is a small saving repeated a dozen times a day.

The second is browser developer tools. Saved breakpoints, network panel filters, and application tab settings are tied to an origin, so under a shared address they bled between projects. A separate name means a separate set of settings that stays where you left it.

The third concerns tools automating work on your machine. A command carrying a name is unambiguous and needs no guessing at a number or reading it from server output, which in scripts and command running assistants removes a source of errors hard to reproduce.

Note, though, that changing an address invalidates saved sessions. Data kept until now under a shared address will not carry over to a new name, so on the first day after switching you must log into every local environment again. That is a one off inconvenience and equally proof that the separation genuinely works.

Portless against the alternatives

OptionStrengthWeaknessPick it when
PortlessZero configuration, HTTPS, framework agnosticYoung project, needs a global installA monorepo with many services
Hosts file entriesWorks everywhere, nothing to installManual editing, no encryptionOne or two fixed addresses
Your own proxyFull control, any configurationConfiguration and upkeep are yoursUnusual networking requirements
A separate internet tunnelAddress reachable by anyoneOne more tool in the chainWhen you would rather not wire a tunnel into the start command

The first row wins when you have several services and start them daily. With one application on a fixed number the gain is small and installing a global tool is not justified.

The last row is a different category, worth remembering when comparing. A tunnel exposes an address to the public internet, which named local addresses do not do on their own. A separate tunnelling tool is not a requirement here, though, since the proxy can launch one for you: the public flag adds an externally visible address alongside the local one rather than replacing it.

Both built in routes to a public address do assume the external service's own program installed and signed in, though, and that is precisely the condition you sometimes do not want to meet. A tunnel needing neither an install nor an account comes out simpler then, such as tunnl.gg raised with a single SSH command. The price is a random subdomain and no stable address, so it fits a one off demo or catching a webhook well, and an address meant to work for a week rather less.

Why local HTTPS stopped being a luxury

The encryption thread deserves expanding, since it is the most underrated reason for reaching for a tool like this.

Browsers have restricted some capabilities to pages considered secure for years. That covers camera and microphone access, geolocation, notifications, offline mechanisms, and several newer interfaces. The localhost address gets lenient treatment in this context, while an address on the local network opened from a phone does not, and that is where the difference hurts most.

The second reason concerns cookies. Settings requiring a secure connection and restricting cookies sent across sites behave differently without encryption. A login that works locally and breaks on a test environment is the classic symptom of that difference, and the cause is often hunted for hours on the wrong side.

The third is mixed content. A page loaded over an encrypted connection will not fetch a resource over an unencrypted one, so an integration with an external service can work in only one of those arrangements. Mirroring production locally removes that whole class of surprise.

The conclusion is simple: the more closely a local environment mirrors production, the fewer bugs surface only after a deploy. A named address with encryption and separate cookies is a cheaper route to that closeness than standing up a full test environment for every developer.

Limitations worth knowing

The project is young, so a few things call for a deliberate decision before wiring it into a whole team's workflow.

A global install means everyone on the team must install it, and versions can drift. For a shared team tool, record in the project documentation which version you assume.

Adding your own certificate authority to the system trust store is an operation that falls under security policy in some organisations. Check that in advance rather than discovering it at a blocked command.

The proxy listens on a port requiring elevated privileges, so the system service runs with administrator rights. That is normal for this class of tool and still worth knowing.

Finally, the local domain name itself is handled inconsistently by older command line tools and libraries. If some script in your project calls the server itself, check it after changing addresses, since some HTTP clients do not trust custom certificates by default.

Introducing it across a team

A globally installed tool needs a different approach from an ordinary project dependency, so a few words on doing it painlessly.

Start with yourself and work that way for a week before proposing it to everyone else. Some problems, a script not trusting the local certificate for instance, surface only through daily use, and discovering them yourself beats discovering them in a meeting.

Then record three things in the repository documentation: the install command, the assumed version, and the exact commands starting each service. Without that third point everyone invents their own names, and a month later nobody knows whether the address in a bug report points at the same service.

Agree the names together and stick to them, since that is where the whole value sits. A name matching a directory in the repository is the simplest convention and requires nobody's memory.

Leave the previous way of starting things working too. The command starting a server without the proxy should still work, so anyone for whom something went wrong can keep working rather than waiting for help. A tool you can bypass is adopted across a team far more easily than one without which nothing starts.

Common mistakes

The first is skipping the system service installation. Addresses stop working after a reboot, and the cause is not obvious.

The second is hardcoding addresses in code. An environment variable settles it once and helps at deployment too.

The third is confusing private network sharing with a public address. The former will not show your application to somebody outside your network, and since both are enabled with similar looking flags it is easy to reach for the wrong one and either show a client nothing or expose to the internet an environment meant to stay inside the team.

The fourth is assuming every tool in the project will accept the local certificate. Scripts calling the server are often the exception and need the certificate authority pointed out to them.

The fifth is introducing the tool across a team without recording it in the project documentation. A new person clones the repository, runs the documented command, and gets a missing program error.

The sixth is using it where there is no problem. One application on one number needs no proxying layer.

FAQ

Does Portless work with every framework?

Yes, because it does not integrate with the framework. You run it before any command that starts a server, and the proxy forwards traffic regardless of what runs underneath. It behaves the same with Next.js, Vite, and a plain Node server.

Where does the HTTPS certificate come from?

On first run a local certificate authority is created and added to the system trust store, and per name certificates are issued from it. Nothing leaves your machine.

Does it replace a tunnel to the internet?

It does not replace one, it builds one in. Beyond the local network and the private overlay network between your own devices, two flags expose a public address: one through the public branch of that same overlay network, the other through an external tunnelling service. Both need that service's own program installed and signed in, and for a longer demo to a client a preview deployment is usually still better, on Vercel for instance, since it does not depend on your machine being switched on.

Does everyone on the team need to install it?

Yes, it is a tool installed globally on a machine rather than a project dependency. Record that requirement in the repository documentation along with the assumed version, so a new person does not get stuck on the first command.

Does it solve the cookie bleeding problem?

Yes, and that is its underrated advantage. Each name is a separate origin as far as the browser is concerned, so cookies and local storage stay separated between projects rather than sharing one space under a common address.

The code and documentation sit in the GitHub repository, and the release list on the project releases page.