Skip to content

New: AI agent integration via Model Context Protocol (MCP).Learn more

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 Secure cookies — browsers won’t store them on http://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=Lax cookies 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.

Terminal window
pnpm dev:setup-portless

That does three things:

  1. portless trust — installs the Portless CA into your OS keychain so the certs it mints are system-trusted.
  2. portless service install — registers Portless as a background service so the proxy is always running, no manual foreground process to babysit.
  3. turbo run setup:portless — walks every workspace that declares a setup:portless script and registers its alias.

Out of the box that registers:

  • https://storefront.evolve.localhosthttp://localhost:3000
  • https://authoring.evolve.localhosthttp://localhost:4007

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).

Terminal window
pnpm dev:secure

That 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.

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 shared evolve.localhost registrable domain.
  • Pick a port that matches the service’s dev script. Portless doesn’t start the service for you — it just proxies what’s already listening.

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.

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.