Files
marketplaces/docs/backend/TENANT-API-DOMAIN-HANDOFF.md
sdarbinyan e5949c3967
Some checks failed
Architecture Governance / architecture (push) Failing after 6m16s
fix(deploy): provision tenant API domains
Reconcile TLS, exact CORS, and backend proxying before release activation so every storefront uses its derived API host.
2026-08-20 15:04:10 +04:00

4.8 KiB

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.<storefront-host>:

  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:

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.<storefront-host>, allow exactly:

Access-Control-Allow-Origin: https://<storefront-host>
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:

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:

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.<full-hostname>.
  • 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.