Portless
Inside Portless: control-plane state and a loopback HTTP data path
Trace launcher registration, per-request routing and HTTP protocol boundaries while separating persisted route management from the proxy library.
What you will learn
- Separate registration from forwarding
- Follow one ordinary request
- Protocols add branches, not a universal guarantee
Before you start
- Basic HTTP origins, ports and command-line concepts
- Ability to distinguish loopback, LAN and public exposure
Trace a named request, choose explicit setup boundaries and separate observed behavior from untested integrations.
Key takeaways
- Route registration and HTTP forwarding are separate components.
- The backend dialer uses loopback addresses rather than arbitrary host DNS.
- Protocol support in source is not an end-to-end browser compatibility result.
Separate registration from forwarding
The launcher and route store manage application names, ports and process ownership. The proxy library consumes a getRoutes callback and invokes it for each request. This interface is important: createProxyServer does not need to know how a CLI inferred the app name or where a caller persisted a route. Our experiment supplied an in-memory list and added a route without restarting the proxy.
RouteStore is the separate persistence component. It validates basic record fields and can filter entries whose recorded process is no longer alive; pid 0 represents a static route. Its locking and cleanup behavior belongs to the control plane, not the HTTP request protocol. A process identifier is also not a permanent application identity, so operational diagnosis should correlate the route with the actual listener and current process.
Follow one ordinary request
For HTTP/1.1, the request Host selects a route; the HTTP/2 compatibility path can use :authority. After matching, the proxy dials the route port through createLoopbackConnection, whose address lookup is explicitly limited to 127.0.0.1 and ::1. The destination hostname supplied by the browser is a routing key, not an arbitrary remote DNS destination for this backend connection.
The handler forwards method, path and body, preserves the original host and builds forwarding headers. Unknown names produce a proxy page, backend connection failures produce a gateway error, and the hop counter catches sufficiently deep forwarding loops. These are different failure boundaries. A useful trace records which name matched and whether the selected application was listening before investigating response rendering.
Protocols add branches, not a universal guarantee
With TLS options, the implementation creates an HTTP/2 secure server with HTTP/1.1 fallback. It includes separate handling for classic WebSocket Upgrade and HTTP/2 extended CONNECT bridged to a backend HTTP/1.1 handshake. The presence of these branches explains the design, but it does not prove every browser and framework hot-reload session succeeds.
The bounded probe exercised only the plain HTTP/1.1 path. It used no certificate, persistent route store, system daemon or external sharing process. This separation makes the results reproducible and easy to interpret: dynamic callback routing is observed, while certificate lifecycle, persistent startup and HMR remain integration tests a deployment owner must perform.
Implementation steps
- 1
Locate the launcher and RouteStore for registration behavior.
- 2
Follow getRoutes into the request handler.
- 3
Trace Host matching and the chosen loopback port.
- 4
Add protocol-specific integration checks without overstating the HTTP probe.
Copy-ready example
Launcher / RouteStore -> getRoutes()
Browser Host -> matching route -> loopback connection -> application
| no match: 404
| backend failure: 502
| hop limit: 508
TLS / HTTP2 / WebSocket branches: inspected, not exercised hereFrequently asked questions
Must every route change restart the proxy?
Not at the library interface: getRoutes is invoked per request. The probe added an in-memory route and reached it without restarting; persistent CLI registration has its own lifecycle.
Does the incoming Host become a remote network destination?
In the inspected backend dialer it selects a registered port, while the socket lookup supplies loopback addresses. Public-sharing integrations are separate components.
Sources
- packages/portless/src/proxy.tsSource checked 2026-09-08
- packages/portless/src/routes.tsSource checked 2026-09-08
- packages/portless/src/utils.tsSource checked 2026-09-08
- packages/portless/src/types.tsSource checked 2026-09-08