Files
web_sport/docs/adr/0015-frontends-reach-the-api-through-their-own-origin.md
2026-08-13 23:20:22 +07:00

3.3 KiB

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.