Local HTTPS with Portless
A handful of Evolve flows need real HTTPS in development:
- OAuth callbacks (CMS sign-in for the authoring editor).
- Anything that sets
Securecookies — browsers won’t store them onhttp://localhost. - Cross-subdomain editor sessions: the chrome on the storefront
probes the authoring service through a sibling subdomain, which
requires both ends to live on the same registrable HTTPS domain
for
SameSite=Laxcookies to ride along.
Evolve uses Portless as the local-HTTPS layer.
It’s a tiny local reverse proxy that terminates TLS in front of each
service and gives every workspace a stable hostname under
*.evolve.localhost. Services keep running on plain HTTP — Portless
fronts them.
One-time setup
Section titled “One-time setup”pnpm dev:setup-portlessThat does three things:
portless trust— installs the Portless CA into your OS keychain so the certs it mints are system-trusted.portless service install— registers Portless as a background service so the proxy is always running, no manual foreground process to babysit.turbo run setup:portless— walks every workspace that declares asetup:portlessscript and registers its alias.
Out of the box that registers:
https://storefront.evolve.localhost→http://localhost:3000https://authoring.evolve.localhost→http://localhost:4007
Why the two-level *.evolve.localhost
Section titled “Why the two-level *.evolve.localhost”A flat *.localhost would work for individual services but breaks
the authoring chrome’s cross-origin session probe. Browsers treat
each .localhost label as a separate registrable domain, so a cookie
set on authoring.localhost is not readable from storefront.localhost
even with SameSite=Lax.
By giving every service a third-level hostname under a shared
evolve.localhost parent, the storefront and the authoring service
share a registrable domain. The editor session cookie travels
SameSite=Lax-safe between them, and the chrome can see who’s
signed in without falling back to SameSite=None (which itself
requires HTTPS — chicken/egg).
Running with HTTPS
Section titled “Running with HTTPS”pnpm dev:secureThat command is the same as pnpm dev plus a banner script that
prints every active Portless route, so you don’t have to grep
package.json to remember which hostname maps to which port.
┌──────────────────────────────────────────────────────────────┐│ Portless routes │├──────────────────────────────────────────────────────────────┤│ https://storefront.evolve.localhost → http://localhost:3000 ││ https://authoring.evolve.localhost → http://localhost:4007 │└──────────────────────────────────────────────────────────────┘Services themselves keep running on plain HTTP. Portless terminates TLS in front of them, so your Fastify and Next.js configs need no HTTPS-specific code paths.
Adding a new service
Section titled “Adding a new service”Add a setup:portless script to the new package’s package.json:
{ "scripts": { "setup:portless": "portless alias <name> <port>" }}Then re-run pnpm dev:setup-portless. The banner script picks the
new route up automatically — it walks every workspace looking for
setup:portless scripts and parses the alias out of the invocation,
so there’s no central registry to keep in sync.
A few conventions:
- Use a dotted name (
authoring.evolve,storefront.evolve) so the result lives under the sharedevolve.localhostregistrable domain. - Pick a port that matches the service’s
devscript. Portless doesn’t start the service for you — it just proxies what’s already listening.
Troubleshooting
Section titled “Troubleshooting”Browser shows the cert as untrusted. You haven’t restarted the
browser since pnpm dev:setup-portless. Quit fully (Cmd-Q, not just
the window) and reopen.
ERR_CONNECTION_REFUSED from a Portless hostname. Portless is
listening on the HTTPS port but the upstream service isn’t running
on its local port yet. Make sure pnpm dev:secure is up.
Cookies still rejected with SameSite=Lax. Check that both ends
of the call use the same *.evolve.localhost parent. A service that
registered itself directly under *.localhost won’t share a
registrable domain with the rest.
Portless service died. portless service status reports the
state; portless service restart recovers it without redoing the
trust step.
Migrating from --experimental-https
Section titled “Migrating from --experimental-https”Earlier Evolve setups used Next.js’s --experimental-https flag to
get a single HTTPS frontend. That works for one service but doesn’t
solve the cross-subdomain story (the authoring service stayed on
plain HTTP, so secure cookies couldn’t cross between them). Switching
to Portless removed the flag from the Next dev script in favor of a
single proxy in front of every service. Drop --experimental-https
when you upgrade — Next.js itself stays on plain HTTP.

