# Final handoff: tenant API domains This is the required production contract between the shared frontend, nginx, and the backend. It supersedes any fixed `api.dexarmarket.ru` or same-origin `/backend` routing proposal. ## 1. Deterministic hostname rule The frontend prefixes the **complete** storefront hostname with `api.`: | Storefront | API origin | Bootstrap | |---|---|---| | `example.com` | `https://api.example.com` | `https://api.example.com/bootstrap` | | `store1.example.com` | `https://api.store1.example.com` | `https://api.store1.example.com/bootstrap` | | `www.example.com` | `https://api.www.example.com` | `https://api.www.example.com/bootstrap` | Bootstrap, auth, legacy routes, and `/api/...` routes all use this origin. Localhost is the only exception and continues through the local `/api` proxy. ## 2. Backend changes required For every request received publicly on `api.`: 1. Behind the trusted project nginx, use `X-Storefront-Host`. nginx deliberately sends the same storefront value as upstream `Host` for compatibility with the currently live backend and preserves the public API hostname in `X-Forwarded-Host`. 2. Without that trusted proxy, normalize the request `Host`: lowercase, remove the port, remove exactly one leading `api.` label when present, and retain every remaining label. 3. Resolve that normalized storefront hostname through the tenant-domain registry. Do not infer a tenant from only the first label. 4. Reject unknown, disabled, or unverified domains with `403` before reading tenant data. Never fall back to the default/Dexar tenant. 5. Bind the authenticated session to the resolved tenant and reject a mismatch. 6. Trust `X-Storefront-Host` / `X-Forwarded-*` only from the known nginx proxy; direct clients can forge them. 7. Return JSON for `/bootstrap`, including a tenant identity that matches the normalized storefront domain. HTML or a default tenant response is a fault. Pseudo-code: ```text if request.remoteAddress is trustedProxy: storefrontHost = lower(stripPort(request.header["X-Storefront-Host"])) else: requestHost = lower(stripPort(request.host)) storefrontHost = removeAtMostOnePrefix(requestHost, "api.") tenant = registry.findVerifiedDomain(storefrontHost) ?? forbidden() request.tenant = tenant ``` ## 3. CORS contract For API host `api.`, allow exactly: ```http Access-Control-Allow-Origin: https:// Access-Control-Allow-Credentials: true Vary: Origin Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS Access-Control-Allow-Headers: Authorization, Content-Type, AdminWebSessionID, X-Requested-With ``` Answer valid preflight requests with `204`. Do not use `*` together with credentials. nginx applies this policy now; the backend should enforce the same allowlist when it is reached without that proxy. ## 4. nginx and TLS Run the idempotent project script as root: ```bash scripts/deploy/configure-api-domain.sh \ --domain gorbushka.market \ --email ops@example.com \ --upstream https://127.0.0.1:445 ``` It creates `api.gorbushka.market`, issues/renews its certificate, configures CORS, and proxies all paths to the backend. Upstream receives `Host: gorbushka.market`, `X-Forwarded-Host: api.gorbushka.market`, and `X-Storefront-Host: gorbushka.market`; the script then reloads nginx and verifies that `/bootstrap` returns a JSON object. For `store1.example.com`, both DNS and TLS must exist for `api.store1.example.com`. A certificate for `*.example.com` does **not** cover that two-label-deep hostname. ## 5. CI/CD contract `deploy.yml` runs the same root-owned configurator before activating a frontend release. Required production secrets: | Secret | Example | |---|---| | `DEPLOY_HOST` | server hostname/IP | | `DEPLOY_USER` | `deploy` | | `DEPLOY_SSH_KEY` | private deploy key | | `DEPLOY_KNOWN_HOSTS` | pinned SSH host-key line | | `STOREFRONT_DOMAINS` | `gorbushka.market store1.example.com` | | `CERTBOT_EMAIL` | operations email | | `BACKEND_UPSTREAM` | `https://127.0.0.1:445` (optional default) | One-time provisioning must first run `server-setup.sh`; it installs the helper as root-owned `/usr/local/sbin/marketplaces-configure-api-domain` and grants the deploy user permission to run only that validated command plus nginx reload. ## 6. Acceptance checks For every storefront domain, all of these must pass: ```bash curl -fsS https://api.example.com/bootstrap | jq -e 'type == "object"' curl -i -X OPTIONS https://api.example.com/bootstrap \ -H 'Origin: https://example.com' \ -H 'Access-Control-Request-Method: GET' ``` - Frontend bundle contains no fixed marketplace API hostname. - Root and nested storefronts call their matching `api.`. - Unknown API hosts return `403`, not the default tenant. - `/bootstrap` returns JSON and the correct tenant. - API responses never return the Angular `index.html` fallback.