Skip to content

The Dev Gateway

The core idea of turnout:

Local apps always talk to localhost (their gateway port). The gateway routes requests to whichever stand the app is currently bound to.

Switching a stand is not editing .env files across repositories - it is one command that changes the app→server binding in one place. Your projects’ env files stay stable and committable, pointing at localhost forever.

The port is written once, in the app’s turnout config. The app receives the address by two roads that back each other up (details on the turnout app page):

  • every command turnout starts for the app - dev, a custom run - carries it in an environment variable whose name the app chooses (VITE_API_URL, REACT_APP_API_URL, …);
  • a dotenv file turnout keeps in step - .env.development.local by default - covers the app when it is started from an IDE or by hand.

The file is read by Vite and CRA in development only, so a production build never sees localhost, and the project’s own .env is never touched.

The gateway has one door per app for the stand - localhost:7100 forwards to whatever myapp is bound to - and one front door for the apps themselves. Dev servers take their port in the order they start, so which app is on 5173 today is anybody’s guess; the front door listens on one well-known port (80, or 7000 where 80 is not to be had) and routes by name: http://myapp.localhost reaches the dev server turnout started for myapp, whichever port it took. The port is turnout’s business: assigned once on the first turnout dev, handed to the server as PORT and {port}, and remembered.

Every name under .localhost resolves to loopback without a hosts file, so turnout open myapp is the whole address book. Details on the turnout gateway page.

The browser talks only to the gateway and holds only the gateway’s own session. Cookies issued by stands are kept inside the gateway, in a separate jar for every app+server pair.

This is what makes switching seamless:

  • switch from staging to prod-eu and back - your staging login session is still alive;
  • stand cookie domains and login redirects never leak into the browser;
  • no more “why am I suddenly logged out” after touching env files.

A stand whose login is a single sign-on page on another domain sends you back to its own address, not to localhost, so that login cannot happen through the gateway. Log in in the browser and hand the session over with turnout cookie set.

A jar keeps cookies the way a browser does (RFC 6265):

  • a cookie the stand deletes is gone - Max-Age=0, or an Expires in the past, is how a stand ends a session, and the cookie stops going back to it;
  • a cookie expires at its Max-Age or Expires; one with neither lasts as long as the gateway runs;
  • Path and Domain decide where it goes - a cookie set for /api is sent to /api and below, not to every path;
  • a Secure cookie goes over HTTPS only (and to a stand on localhost, as browsers do);
  • a cookie for another domain is refused, as a browser refuses it.

Cookies handed over with cookie set carry no attributes - a Cookie header has none - so they are kept for the stand’s whole host, every path, for the session.

Whether a session is still alive now is a question for the stand. turnout doctor asks it: the gateway sends again the last GET that proved the session and reports what the stand answered.

Real stands are messy, so the gateway is built for:

  • HTTPS with self-signed certificates - TLS verification is configured per server;
  • redirects - Location headers are rewritten so the browser never escapes to the stand’s real address;
  • WebSocket and live streams;
  • clear errors - if a stand is down, you see “stand unreachable”, not a cryptic proxy failure - and the gateway says so outside the browser too.

Response bodies are not rewritten: apps are expected to use relative URLs for API calls, which is the norm for SPA setups.

The gateway sees every answer a stand gives. Two of them the app in the browser cannot tell from its own bugs, so the gateway says them out loud:

Signal When Gone when
The stand is down It answers 502, 503 or 504 - its web server is there, the application is not - or does not answer at all It answers anything below 500 again
The session expired It answers 401 (or IIS’s 440) to a request that carried the cookies the gateway holds for it A login through the gateway sets new cookies, or turnout cookie set hands it a session

A 401 on a request that carried no cookies is not a lapsed session - nobody had logged in yet, and the app’s own login page is the answer. A stand that is down is down for every app pointed at it, so it is one signal; a session belongs to one app.

A signal shows in three places:

  • turnout status - a line under the gateway, with how long it has been up;
  • the prompt line - prod 502, prod down, prod login;
  • one desktop notification - when it goes up, not on every failing request. staging answers 502 opens the stand; myapp: the staging session expired opens the app, where the login page is. A stand that flaps is not announced again for ten minutes; status still shows every episode.

The gateway’s log - turnout logs gateway - has a line for every signal that goes up and down, so an afternoon of outages can be read back later.

Terminal window
turnout gateway start # once per workday
turnout use myapp staging # bind the app to a stand
turnout dev myapp # the dev server, on the port turnout gave it
turnout open myapp # http://myapp.localhost
turnout use myapp prod-eu # switch - no restarts, no env edits

See turnout use and turnout gateway for details.