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

74 lines
3.3 KiB
Markdown

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