Using pwt ports as a central registry for every app on your machine

Dozens of small projects accumulate on a laptop, generated faster than ever, and every framework template ships with the same default: 3000, 8000, 8888. Nothing coordinates them, so the question “which port does this project use” has no answer except trying one.

The ask that follows is always some version of “a central registry and routing for all this”. That sounds like one feature. It is two, at different layers, and every tool solves one and leaves the other, which is why nothing ever feels finished:

Layer 1 is bookkeeping, and scanning is not it

The registry question is “who owns 8001”. The universal answer, and the one every framework and helper script implements, is to scan: try a port, see if it is bound, take the next one if it is. devenv’s ports.<n>.allocate does a careful version of this, holding ports during evaluation to avoid races.

Scanning has one blind spot, and it is fatal for this scenario: a stopped server still owns its port. Thirty projects on a laptop are not running at once; most are asleep. Every scan looks at a machine where almost nothing is bound and concludes almost everything is free, so two projects get the same number and the collision is deferred to whichever morning you start both.

I had this exact hole in my own tool, which allocated per project and consulted the live machine. Two projects, both configured to start at 8000, nothing running:

$ # project chatapp, then project dashboard
   chatapp    feature-a    8001
   dashboard  feature-b    8001

Both correct by their own logic, both wrong. A port is a machine resource, so the record has to be machine-wide: allocation must consult what every project has already been given, whether or not anything is listening today. After the fix, on the same machine:

$ pwt ports
PORT    PROJECT     WORKTREE      STATUS
8000    chatapp     @             conflict
8000    dashboard   @             conflict
8001    chatapp     feature-a     conflict
8001    dashboard   feature-b     conflict
8002    chatapp     feature-c     -
8003    dashboard   feature-d     -
8004    chatapp     feature-e     -

Two records share a port. Fix with: pwt fix-port <worktree>

New allocations step around every other project’s numbers, and the pre-existing overlaps are reported rather than quietly inherited. That listing is the “central registry for this shit”: one command, every project, plus who is actually listening right now.

The important part is not the tool, it is the shape: a registry is a file you write to when you hand out a port, not a scan you run when you need one. Anything that only scans will keep handing out duplicates to a machine full of sleeping apps.

Repairing an old overlap has one wrinkle, and it decides whether the fix is free. Two patterns exist in the wild:

# Read at runtime: the port is whatever the tool says, today
server() { exec env PORT="$PWT_PORT" mix phx.server; }

# Baked at setup: the number is written into generated files
# .env       DEFAULT_URL=localhost:5001
#            WORKTREE_DB_SUFFIX=_wt5001
# Procfile   rails s -p 5001

Moving a project of the first kind is a metadata edit and nothing else. Moving one of the second kind leaves generated files pointing at the old number until the setup hook runs again, and when a database name is derived from the port, as in that second block, reallocating quietly points the app at a different database. Check which kind you have before repairing a port that something already generated files from.

Repairing an old overlap has one wrinkle worth knowing, and it decides whether the fix is free or not. Two patterns exist in the wild:

# Read at runtime: the port is whatever the tool says, today
server() { exec env PORT="$PWT_PORT" mix phx.server; }

# Baked at setup: the number is written into generated files
# .env         DEFAULT_URL=localhost:5001
#              WORKTREE_DB_SUFFIX=_wt5001
# Procfile     rails s -p 5001

Moving a project of the first kind is a metadata edit and nothing else. Moving one of the second kind means the generated files still point at the old number until the setup hook runs again, and if a database name is derived from the port, as in that second block, reallocating quietly points the app at a different database. Check which kind you have before you fix a port that something already generated files from.

Layer 2 is naming, and it cannot be solved by allocation

Even with a perfect registry you still have the other half of the complaint: you have to remember that the dashboard is 8003. The fixes for that live one layer up, and they are genuinely different tools:

Approach Gives you Costs
Path routing (/dashboard) one host, one port apps must tolerate a path prefix; many break on absolute URLs
Per-app subdomain (app.localhost) clean names, separate cookie jars a proxy; *.localhost works in Chrome and Firefox without /etc/hosts
CNAME / real DNS works off-machine DNS records and certificates for a laptop
One stable port, repointed one address that never changes only one app is live at a time

The last row is the one people skip, and it is the right shape when the thing on the other end is not you: an OAuth callback, a webhook, a device on your network. Those store one address and have no opinion about your ports, which is its own post.

Notice that none of these rows allocate anything. A proxy routes a name to a port that something else decided. That is why the two layers keep getting conflated and nothing feels solved: tools that name (portless, Port Zero, Caddy setups) do not stop two apps from claiming 8000, and tools that allocate (devenv, per-project config) do not give you a name worth typing.

The honest scope

For a laptop with dozens of unrelated projects, the registry half is the half that pays: you need the machine to remember what it handed out, and you need to see the map. That is pwt ports, and the allocation behind it, in pwt.

The honest caveat: pwt is a git worktree manager. The port registry is real and now machine-wide, but you get it by registering a project with pwt init, which earns its keep when you also want per-worktree setup, servers and jobs. If all you want is a port broker for 35 loose folders, this is a bigger tool than the problem. What I would keep from it, whatever you use, is the rule: write down the allocation, do not scan for it.

brew install jonasporto/pwt/pwt, then pwt ports (reference).

You do not have to memorize any of this. pwt skill prints the agent-facing guide to pwt; tell your agent the outcome you want and point it there, and it works out the mechanism and verifies it in a throwaway worktree.

Ask an agent for “a free port for this project” and it will scan for one, which is the wrong answer: a stopped server owns its port too. The guide points it at the allocation that already happened. Reference: pwt ports.