Skip to content

fix(portal): a page served from a real host never calls localhost (bd-2559) - #190

Merged
hatafatif merged 1 commit into
developfrom
bd-2559-apibase
Aug 12, 2026
Merged

fix(portal): a page served from a real host never calls localhost (bd-2559)#190
hatafatif merged 1 commit into
developfrom
bd-2559-apibase

Conversation

@hatafatif

Copy link
Copy Markdown
Collaborator

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

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     -> isProd:!1  + "http://localhost:4000/..." baked in
NODE_ENV=production  -> isProd:!0

Confirmed live — from the same build command:

STAGING  resolveApiBaseUrl({isNative:BO(),isProd:!1   ← broken
PROD     resolveApiBaseUrl({isNative:_C(),isProd:!0   ← fine

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

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

NODE_ENV is left alone — the code no longer depends on it being any particular string, which is the point.

Verification

  • Red-first: 2 failing → 8 passing. The 6 that passed from the start prove dev behaviour was already correct and stays that way.
  • 22 suites / 263 tests + 13 files / 68 component tests green.
  • Rebuilt with NODE_ENV=staging and confirmed the compiled ternary is now isProd || isServedByRealHost(origin) ? "/api/portal" : ... — the localhost string remains only as an unreachable branch for the dev case.

Closes: bd-2559

🤖 Generated with Claude Code

…-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
@hatafatif
hatafatif merged commit 784e863 into develop Aug 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant