Reconcile TLS, exact CORS, and backend proxying before release activation so every storefront uses its derived API host.
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>:
- Behind the trusted project nginx, use
X-Storefront-Host. nginx deliberately sends the same storefront value as upstreamHostfor compatibility with the currently live backend and preserves the public API hostname inX-Forwarded-Host. - Without that trusted proxy, normalize the request
Host: lowercase, remove the port, remove exactly one leadingapi.label when present, and retain every remaining label. - Resolve that normalized storefront hostname through the tenant-domain registry. Do not infer a tenant from only the first label.
- Reject unknown, disabled, or unverified domains with
403before reading tenant data. Never fall back to the default/Dexar tenant. - Bind the authenticated session to the resolved tenant and reject a mismatch.
- Trust
X-Storefront-Host/X-Forwarded-*only from the known nginx proxy; direct clients can forge them. - 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. /bootstrapreturns JSON and the correct tenant.- API responses never return the Angular
index.htmlfallback.