--- id: ADR-0004 title: Route every tenant API through a same-origin gateway status: active date: 2026-08-20 supersedes: [] tags: [architecture, multi-tenant, api, routing, nginx] --- # ADR-0004: Route every tenant API through a same-origin gateway ## Context The production bundle embedded `api.dexarmarket.ru` and constructed unknown tenant URLs as `https://{tenant}.api.dexarmarket.ru:445`. Bootstrap used a different route (`/bootstrap` on the storefront origin), while auth had its own fixed API base. A custom domain could therefore use three different backend origins. Directly calling the backend port is not a safe fallback: production returned `403` for both a bootstrap request carrying `Origin: https://gorbushka.market` and the corresponding CORS preflight. Meanwhile an unmatched `/bootstrap` on the storefront nginx server fell through to `index.html`, producing a misleading HTTP 200 with HTML instead of bootstrap JSON. ## Decision Every tenant frontend uses one API base derived at runtime from the browser origin: `{origin}/backend`. - Bootstrap loads from `{origin}/backend/bootstrap`. - Auth receives the same base through the `AUTH_API_URL` provider. - Legacy endpoints append their existing paths to the same base. - Versioned `/api/...` endpoints retain the `/api` prefix when routed. - nginx owns `/backend/`, removes that prefix when proxying, and forwards the original `Host`, `X-Forwarded-For`, and `X-Forwarded-Proto` headers upstream. The frontend contains no tenant/domain allowlist and no production backend hostname. Tenant selection remains a server-side responsibility based on the verified forwarded host. ## Consequences Browser traffic is same-origin, so custom domains do not require per-tenant CORS configuration and one bundle works for every attached domain. Bootstrap, auth, legacy routes, and versioned routes cannot silently drift to different hosts. Every nginx tenant/catch-all configuration must include the `/backend/` gateway. Deploy verification must check that `/backend/bootstrap` returns JSON rather than accepting a generic HTTP 200 from the SPA fallback. The upstream must still reject unknown hosts; the gateway preserves `Host` but does not authenticate a tenant by itself.