Merge branch 'B2B'
This commit is contained in:
@@ -1,12 +1,14 @@
|
||||
# Deployment — server provisioning, CD, TLS
|
||||
|
||||
Frontend only. The backend service (`:8080`) is a separate developer's responsibility; nginx proxies the same-origin `/backend/` gateway (and the legacy direct `/api/` route) to it and will `502` until it exists.
|
||||
Frontend only. The backend service (`:8080`) is a separate developer's responsibility. API hostnames are separate reverse proxies and will return `502` until their upstream exists.
|
||||
|
||||
**Multi-tenant, one bundle.** Every customer domain is served by the same build. The SPA resolves its tenant from the `Host` header ([BACKEND-HANDOFF §1a](backend/BACKEND-HANDOFF.md)). One deploy updates every domain simultaneously — there is no per-tenant build and no per-tenant deploy.
|
||||
|
||||
All browser API traffic uses `{origin}/backend/...`. nginx removes `/backend/`
|
||||
before forwarding and preserves `Host`; do not replace this with a hardcoded API
|
||||
domain or a direct backend port, because that breaks custom domains and CORS.
|
||||
The SPA derives its API origin from the complete storefront hostname:
|
||||
`example.com` uses `api.example.com`, and `store1.example.com` uses
|
||||
`api.store1.example.com`. Provision DNS, TLS, reverse proxying, and CORS for
|
||||
every derived API hostname. A single-label wildcard certificate does not cover
|
||||
nested names such as `api.store1.example.com`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -10,11 +10,11 @@ Single entry point for a backend developer picking this up cold. Written 2026-08
|
||||
|
||||
One deployed bundle serves **every customer domain**. There is no per-tenant build. The chain is:
|
||||
|
||||
1. [`TenantResolverService`](../../src/app/core/config/tenant-resolver.service.ts) reads the current browser hostname/origin (localhost still uses the development proxy).
|
||||
2. [`ApiConfigService`](../../src/app/core/config/api-config.service.ts) resolves one same-origin API base: `{origin}/backend`.
|
||||
1. [`TenantResolverService`](../../src/app/core/config/tenant-resolver.service.ts) reads the complete current browser hostname and protocol (localhost still uses the development proxy).
|
||||
2. [`ApiConfigService`](../../src/app/core/config/api-config.service.ts) prefixes that hostname with `api.`: `example.com` becomes `api.example.com`; `store1.example.com` becomes `api.store1.example.com`.
|
||||
3. `ApiBootstrapProvider`, auth, legacy API calls, and versioned `/api/...` calls all use that same base.
|
||||
4. nginx owns `/backend/`, strips that prefix, proxies to the backend, and preserves the original `Host` so the backend can resolve the tenant server-side.
|
||||
5. nginx is `default_server` / `server_name _`, so any domain pointed at the server IP gets the same bundle and self-resolves.
|
||||
4. Each derived API hostname owns DNS, TLS, CORS, and a reverse proxy to the backend.
|
||||
5. Backend tenant lookup recognizes `api.<storefront-host>` as the API alias of `<storefront-host>`; frontend nginx remains `default_server` / `server_name _`, so any attached storefront domain receives the same bundle.
|
||||
|
||||
**What this means for you:** the bootstrap endpoint is the single most important thing to build after auth. Every request must be tenant-scoped server-side, and a tenant must never be able to read another tenant's data — return `403`, not an empty result (see [TRACK-S §2](TRACK-S-SECURITY-RBAC-CONTRACT.md)). The frontend supplies the tenant identity from the hostname; the backend must treat that as an untrusted hint and derive real scope from the authenticated session.
|
||||
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
---
|
||||
id: ADR-0004
|
||||
title: Derive each API host from the complete storefront host
|
||||
status: active
|
||||
date: 2026-08-20
|
||||
supersedes: []
|
||||
tags: [architecture, multi-tenant, api, routing, dns]
|
||||
---
|
||||
|
||||
# ADR-0004: Derive each API host from the complete storefront host
|
||||
|
||||
## Context
|
||||
|
||||
One production bundle serves root domains and arbitrary storefront subdomains.
|
||||
The old bundle embedded `api.dexarmarket.ru`, while an earlier correction used
|
||||
a same-origin `/backend` gateway. Neither expresses the required domain rule:
|
||||
each storefront has a corresponding API hostname derived from its full host.
|
||||
|
||||
Examples:
|
||||
|
||||
- `example.com` uses `api.example.com`.
|
||||
- `store1.example.com` uses `api.store1.example.com`.
|
||||
|
||||
Bootstrap, auth, legacy endpoints, and versioned endpoints must not use
|
||||
different base-host selection rules.
|
||||
|
||||
## Decision
|
||||
|
||||
At runtime the frontend prefixes the complete browser hostname with `api.` and
|
||||
keeps the browser protocol: `{protocol}//api.{hostname}`.
|
||||
|
||||
- Bootstrap loads from `https://api.{hostname}/bootstrap`.
|
||||
- Auth receives the same derived base through `AUTH_API_URL`.
|
||||
- Legacy endpoints append their existing paths to that base.
|
||||
- Versioned `/api/...` endpoints retain the `/api` prefix.
|
||||
- Localhost and loopback continue to use the local `/api` development proxy.
|
||||
- An explicit `tenantApiBaseUrls` entry may override the convention for an
|
||||
exceptional host, without changing the shared bundle.
|
||||
|
||||
The complete hostname is preserved. In particular, `www.example.com` maps to
|
||||
`api.www.example.com`; no label is stripped or interpreted by the frontend.
|
||||
|
||||
## Consequences
|
||||
|
||||
One artifact works on root domains and nested storefront subdomains without a
|
||||
tenant allowlist or per-domain build. Every API hostname must have DNS, TLS, a
|
||||
working reverse proxy, and CORS configured for its corresponding storefront.
|
||||
|
||||
A wildcard such as `*.example.com` does not cover the multi-label hostname
|
||||
`api.store1.example.com`; nested API names need explicit certificates/DNS or a
|
||||
certificate and routing strategy that covers that depth. Backend tenant lookup
|
||||
must recognize `api.<storefront-host>` as the API alias of `<storefront-host>`.
|
||||
@@ -1,51 +0,0 @@
|
||||
---
|
||||
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.
|
||||
@@ -11,4 +11,4 @@
|
||||
{"id":"PV-20260818T104300Z-b3c4","subject":"RoutingContext","predicate":"is-required-on","object":"CheckoutSession, PaymentIntent, Payment, Refund and ReconciliationRecord; frozen at checkout-session creation and immutable thereafter, so a payment is always attributable to exactly one payment point","src":["docs/backend/PHASE-1-MONEY-FX-PAYMENTS-CONTRACT.md","docs/backend/PHASE-7-PAYMENTS-RECONCILIATION-CONTRACT.md"],"status":"active","kind":"constraint","updated_at":"2026-08-18T10:43:00Z","confidence":"high","tags":["payments","reconciliation","contract"]}
|
||||
{"id":"PV-20260818T104400Z-d9e2","subject":"partner-api-credentials","predicate":"are-scoped-by","object":"a single node whose subtree defines authority; we hold only the partner-generated public key, rotation runs on a bounded overlap window and revocation is immediate and irreversible","src":["docs/backend/PARTNER-PROVISIONING-API-CONTRACT.md","docs/backend/TRACK-S-SECURITY-RBAC-CONTRACT.md"],"status":"active","kind":"decision","updated_at":"2026-08-18T10:44:00Z","confidence":"high","tags":["security","credentials","partner"]}
|
||||
{"id":"PV-20260818T104500Z-a6f7","subject":"checkout-payment-methods","predicate":"already-support","object":"both qr and card end to end in src/app/pages/cart/cart.component.ts (separate create paths and separate status pollers); card is not an outstanding gap","src":["src/app/pages/cart/cart.component.ts","src/app/services/api.service.ts"],"status":"active","kind":"implemented","updated_at":"2026-08-18T10:45:00Z","confidence":"high","tags":["payments","frontend"]}
|
||||
{"id":"PV-20260820T095500Z-b17e","subject":"tenant-api-routing","predicate":"is-decided-to-use","object":"one same-origin {origin}/backend gateway for bootstrap, auth, legacy endpoints, and versioned /api endpoints; nginx strips /backend and preserves the original Host for server-side tenant resolution","src":["docs/context/adrs/ADR-0004-route-every-tenant-api-through-a-same-origin-gateway.md","src/app/core/config/api-config.service.ts","scripts/deploy/server-setup.sh"],"status":"active","kind":"decision","updated_at":"2026-08-20T09:55:00Z","confidence":"high","tags":["architecture","multi-tenant","api","routing","nginx"]}
|
||||
{"id":"PV-20260820T095500Z-b17e","subject":"tenant-api-routing","predicate":"is-decided-to-use","object":"a runtime-derived API origin that prefixes the complete storefront hostname with api.; example.com maps to api.example.com and store1.example.com maps to api.store1.example.com for bootstrap, auth, legacy, and versioned endpoints","src":["docs/context/adrs/ADR-0004-derive-api-host-from-storefront-host.md","src/app/core/config/api-config.service.ts"],"status":"active","kind":"decision","updated_at":"2026-08-20T10:31:00Z","confidence":"high","tags":["architecture","multi-tenant","api","routing","dns"]}
|
||||
|
||||
Reference in New Issue
Block a user