fix(portal): a page served from a real host never calls localhost (bd-2559) - #190
Merged
Conversation
…-2559) Staging login was broken: the OPTIONS preflight went to http://localhost:4000/api/portal — a host that does not exist for the user — so it failed before any request was sent. resolveApiBaseUrl fell back to the dev API URL whenever isProd was false. isProd is Vite's import.meta.env.PROD, baked in at BUILD time from NODE_ENV, and the Railway staging portal service sets NODE_ENV=staging — not the literal "production". So PROD was false and every staging build shipped a hardcoded localhost URL. Reproduced deterministically: NODE_ENV=staging emits `isProd:!1` and the localhost string; NODE_ENV=production emits `isProd:!0`. Confirmed live — staging's index-pJWcDW7H.js had isProd:!1 while prod's index-BAEpfg8z.js had isProd:!0, from the SAME build command. PRE-EXISTING, not caused by the Railway build pipeline (bd-2555): the old hand-copied bundle carries the same string. The pipeline only made it reachable, by finally rebuilding staging. Production escaped because its service happens to set NODE_ENV=production — luck, not design. One env-var typo away from shipping a dev URL to teachers, silently, on any deploy. The fallback exists for `vite dev`, where the SPA is served from localhost:5173 while the API runs separately on :4000. So the real signal is WHERE THE PAGE CAME FROM, not what NODE_ENV said at build time: a page served by a real remote host is served by something that also serves the API, so same-origin is correct. Gated on isServedByRealHost(origin), the same predicate bd-2554 already uses to tell a real host from a Capacitor shell — one definition of "is this a real host", not two. staging build (isProd=false) on the real host -> /api/portal prod build on the real host -> /api/portal vite dev on localhost:5173 -> http://localhost:4000/api/portal Tests red-first: 2 failing before, 8 passing after; 22 suites / 263 tests green plus 13 files / 68 component tests. NODE_ENV is left alone — the code no longer depends on it being any particular string. Closes: bd-2559
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Staging login was broken — the OPTIONS preflight went to
http://localhost:4000/api/portal, a host that doesn't exist for the user, so it failed before any request was sent.Root cause
resolveApiBaseUrlfell back to the dev API URL wheneverisProdwas false.isProdis Vite'simport.meta.env.PROD, baked in at build time fromNODE_ENV— and the Railway staging portal service setsNODE_ENV=staging, not the literal"production".So
PRODwas false and every staging build shipped a hardcoded localhost URL.Reproduced deterministically:
Confirmed live — from the same build command:
Two things worth being clear about
This is pre-existing, not caused by the Railway build pipeline (bd-2555). The old hand-copied bundle carries the same string. The pipeline only made it reachable, by finally rebuilding staging after months of a frozen artifact.
Production escaped by luck, not design. Its service happens to say
NODE_ENV=production. One env-var typo away from shipping a dev URL to teachers, silently, on any deploy.The fix
The fallback exists for
vite dev, where the SPA is served fromlocalhost:5173while the API runs separately on:4000. So the real signal is where the page came from, not whatNODE_ENVsaid at build time — a page served by a real remote host is served by something that also serves the API.Gated on
isServedByRealHost(origin)— the same predicate bd-2554 already uses to tell a real host from a Capacitor shell. One definition of "is this a real host", not two.NODE_ENVis left alone — the code no longer depends on it being any particular string, which is the point.Verification
NODE_ENV=stagingand confirmed the compiled ternary is nowisProd || isServedByRealHost(origin) ? "/api/portal" : ...— the localhost string remains only as an unreachable branch for the dev case.Closes: bd-2559
🤖 Generated with Claude Code