cherrypicker: names for your ports
localhost:4000 means nothing. localhost:5173 means nothing. Six dev
servers into an afternoon, no port number means anything, and the one you
bookmarked yesterday is someone else now. Vercel's
portless named this problem
properly: dev servers should live at stable named URLs like
http://docs.localhost, and the port lottery should be the proxy's problem.
We wanted that for Cherry. We did not want it enough to install it.
Why not just use portless
portless is a global npm install that self-elevates to bind port 443 and installs a locally trusted root certificate authority. Each of those is defensible on its own; together they are a lot of blast radius for a convenience tool, sitting in the npm supply chain, which is the most attacked package ecosystem there is. It also works by wrapping your dev server as a child process and injecting the right port flag for each framework it recognises, which means a compatibility list that has to chase every CLI's flag changes forever.
So we built cherrypicker in a day: the same idea, on the BEAM, with the opposite instincts. Two runtime dependencies (Bandit and Finch), no Node anywhere, no certificate authority until TLS ships as an explicit opt-in verb, and no process wrapping at all.
The register model
The design bet is one sentence: apps register, nothing gets wrapped. Your dev server starts however it starts. Then something tells the daemon where it is:
$ cherrypicker route docs 8080
http://docs.localhost
That is the whole contract. The daemon is a loopback reverse proxy reading
the Host header: docs.localhost looks up docs in an ETS table and
streams the request to 127.0.0.1:8080. Names resolve for free because
*.localhost already points at loopback on Windows, macOS and systemd
Linux. No DNS, no hosts file, nothing listening beyond your own machine.
Registering an existing name replaces its port, which is exactly what a dev
server restarting on a new ephemeral port wants. And discovery is a file,
not configuration: the daemon writes its bound port to
~/.cherrypicker/daemon.json on start and removes it on shutdown. A client
reads the file, speaks a four-endpoint JSON control API, and if no daemon
answers it gets {:error, :no_daemon} and carries on with plain port URLs.
Degradation is a designed path, not an error.
The proxy streams responses chunk by chunk in both directions, which matters more than it sounds: server-sent events survive it, so a dev server's live reload keeps working through the named URL. We proved that the fun way, by serving this site's source through it and watching an edit push a reload event through the proxy.
Where cherry comes in
The next Cherry release grows two small serve features that make this
seamless. cherry serve now honours the PORT environment variable when
--port is absent, the convention every proxy runner and PaaS-style tool
already speaks. And it gains a flag:
$ cherry serve --name mysite
Serving with live reload at http://mysite.localhost — Ctrl-C to stop.
With a cherrypicker daemon running, serve registers its bound port and prints the named URL. Without one, you get the port URL as ever; the feature costs one failed connect and never a failed serve. If the daemon refuses the name, serve says why on stderr and falls back.
The interesting implementation choice is what Cherry does not do: it
takes no dependency on cherrypicker. The client inside Cherry is about
eighty lines of standard library, :httpc plus the built-in JSON
module, speaking the same control API. A dependency would drag a proxy
stack into every Cherry install for a feature most serves never touch, and
the whole point of the register model is that the contract is small enough
to not need a library. Elixir apps that do want the packaged client can
take cherrypicker from Hex and
call Cherrypicker.register/2 themselves; the daemon does not care which
kind of client is talking.
How it will get used
The loop we are aiming at, once the release lands:
$ cherrypicker start
proxy up — routes serve at http://<name>.localhost (Ctrl-C to stop)
$ cherry serve --name blog
Then http://blog.localhost is your writing loop, today and next month,
whatever port serve actually bound. Agents get the same stability through
the --json envelopes on both tools, which is worth spelling out: a
stable URL is a small quality-of-life win for a human and a genuinely
load-bearing one for an agent, which otherwise has to re-discover the port
every session.
cherrypicker's own site, including the full API reference, is at holsee.github.io/cherrypicker, and yes, it is built with Cherry. Dogfooding it immediately paid for itself: building a Cherry site that takes Cherry from Hex surfaced an overlay-resolution bug no repo-local site could ever hit, fixed the same day. Tools you build for yourself are tools you debug for everyone.