Stage M2
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
# 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.
|
||||
@@ -23,6 +23,7 @@ An ADR is immutable once accepted. If a decision changes, add a new ADR that sup
|
||||
| [0012](./0012-postgresql-full-text-search-before-a-dedicated-search-engine.md) | PostgreSQL full-text search before a dedicated search engine | Accepted |
|
||||
| [0013](./0013-content-translations-in-typed-tables-ui-strings-in-message-catalogs.md) | Content translations in typed tables, UI strings in message catalogs | Accepted |
|
||||
| [0014](./0014-denormalised-price-projection-on-product.md) | A denormalised price/stock projection on Product | Accepted |
|
||||
| [0015](./0015-frontends-reach-the-api-through-their-own-origin.md) | Frontends reach the API through their own origin | Accepted |
|
||||
|
||||
## Decisions deliberately NOT recorded yet
|
||||
|
||||
|
||||
Reference in New Issue
Block a user