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.
How the app learns the address
Section titled “How the app learns the address”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 customrun- 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.localby 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.
Two doors
Section titled “Two doors”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.
Cookie jar per app+server
Section titled “Cookie jar per app+server”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
stagingtoprod-euand back - yourstaginglogin 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 anExpiresin the past, is how a stand ends a session, and the cookie stops going back to it; - a cookie expires at its
Max-AgeorExpires; one with neither lasts as long as the gateway runs; PathandDomaindecide where it goes - a cookie set for/apiis sent to/apiand below, not to every path;- a
Securecookie goes over HTTPS only (and to a stand onlocalhost, 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.
What the gateway handles
Section titled “What the gateway handles”Real stands are messy, so the gateway is built for:
- HTTPS with self-signed certificates - TLS verification is configured per server;
- redirects -
Locationheaders 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.
When a stand misbehaves
Section titled “When a stand misbehaves”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;
statusstill 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.
Daily flow
Section titled “Daily flow”turnout gateway start # once per workdayturnout use myapp staging # bind the app to a standturnout dev myapp # the dev server, on the port turnout gave itturnout open myapp # http://myapp.localhostturnout use myapp prod-eu # switch - no restarts, no env editsSee turnout use and turnout gateway for details.