# ADR-0015: Frontends reach the API through their own origin - **Status:** Accepted - **Date:** 2026-08-12 ## Context The refresh token is delivered as an httpOnly cookie (ADR-0008) — that is what stops an XSS from stealing the credential that mints new sessions. Cookies are governed by `SameSite`. If the browser calls `api.example.com` from `admin.example.com`, that is a cross-site request, and the cookie only rides along with `SameSite=None`. `SameSite=None` requires `Secure`, which requires HTTPS. Local development runs on plain HTTP, so the cookie would simply never be set — presenting as a login that "succeeds" and then immediately forgets you. The alternatives are all worse: put the refresh token in `localStorage` (readable by any script, which defeats the entire httpOnly design), run local development over self-signed HTTPS (friction on every machine and in CI), or accept that dev and production authenticate differently (the class of bug you only find in staging). ## Decision **The browser always calls the API on the same origin as the page it is on.** - Production: Nginx already routes `/api/` to the API container on the same host — this was in the topology from milestone 0. - Development: a Next.js `rewrites()` entry maps `/api/:path*` to `API_INTERNAL_URL`, reproducing that topology exactly. - `browserApi` is therefore created with `baseUrl: ''`, and `HttpClient` resolves a relative base against `window.location.origin`. Server-side rendering is unaffected: it calls `API_INTERNAL_URL` directly over the internal network, because there is no cookie and no browser involved. The cookie is then first-party, `SameSite=Lax`, `httpOnly`, `Secure` in production, and scoped to `path=/api/v1/auth` so it is attached to two endpoints rather than every API call. ## Consequences Development and production share one authentication topology, so a cookie problem is reproducible locally instead of appearing first in staging. CORS effectively disappears for browser traffic — the requests are same-origin. The API's CORS config remains for non-browser and tooling access. Storefront and admin get separately named cookies (`sport_refresh`, `sport_admin_refresh`). Sharing a name would mean signing into the admin silently replaced a customer session in the same browser. The costs, stated plainly: - One extra network hop in development (browser → Next → API). Irrelevant locally, and absent in production where Nginx was already the front door. - The frontends now have a route namespace (`/api/*`) they must not use for their own route handlers. Worth noting in review; neither app has any. - `API_INTERNAL_URL` becomes required for the dev rewrite, not just for SSR. ## Alternatives considered **Refresh token in `localStorage`.** Removes the cookie problem entirely and removes the security property with it — any injected script can read it. Rejected outright. **`SameSite=None; Secure` with HTTPS in development.** Correct, but forces every developer and CI job to trust a local certificate. Rejected as friction that buys nothing production does not already provide. **A dedicated auth subdomain with a parent-domain cookie.** Works in production, but `localhost` has no usable parent domain, so development still diverges. Rejected for the same reason as above.