Merge branch 'B2B'
Some checks failed
Architecture Governance / architecture (push) Failing after 6m33s
Deploy Frontend / deploy (push) Failing after 2m53s

This commit is contained in:
2026-08-20 15:05:57 +04:00
8 changed files with 315 additions and 6 deletions

View File

@@ -1,6 +1,9 @@
# Deployment — server provisioning, CD, TLS
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.
Frontend deployment plus API-domain edge configuration. The backend service is a
separate developer's responsibility. API hostnames are separate reverse proxies
and return `502` until their configured upstream exists (production currently
defaults to `https://127.0.0.1:445`).
**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.
@@ -18,6 +21,7 @@ nested names such as `api.store1.example.com`.
|---|---|
| `scripts/deploy/server-setup.sh` | One-time server provisioning. Idempotent. Run as root. |
| `scripts/deploy/add-domain.sh` | Attach one domain + issue TLS. Run per domain, as root, after DNS resolves. |
| `scripts/deploy/configure-api-domain.sh` | Configure `api.<full storefront host>` TLS, exact CORS, backend proxy, and JSON bootstrap verification. |
| `.github/workflows/deploy.yml` | CD: build → upload → atomic swap → verify. Triggers on push to `main`. |
---
@@ -56,7 +60,10 @@ Copy `scripts/deploy/` to the server and run:
sudo bash server-setup.sh --pubkey "$(cat marketplaces_deploy.pub)"
```
This installs nginx + certbot, creates a **key-only** `deploy` user with no password, writes the catch-all nginx config, opens 80/443/OpenSSH in ufw, and grants `deploy` exactly one sudo right: `systemctl reload nginx`.
This installs nginx + certbot, creates a **key-only** `deploy` user with no
password, writes the catch-all nginx config, opens 80/443/OpenSSH in ufw, and
installs a root-owned, argument-validating API-domain helper. The deploy user may
run that helper and reload nginx, but cannot replace the helper.
Verify before continuing:
@@ -82,6 +89,15 @@ The output is the `DEPLOY_KNOWN_HOSTS` secret. Pinning it means a rebuilt or imp
| `DEPLOY_USER` | `deploy` |
| `DEPLOY_SSH_KEY` | contents of the **private** key file |
| `DEPLOY_KNOWN_HOSTS` | output of `ssh-keyscan -H <server-ip>` |
| `STOREFRONT_DOMAINS` | space-separated full hosts, e.g. `gorbushka.market store1.example.com` |
| `CERTBOT_EMAIL` | operations email used for Let's Encrypt |
| `BACKEND_UPSTREAM` | optional; defaults to `https://127.0.0.1:445` |
Before deploying, point every derived API hostname at the server. For the
example above, DNS must resolve both `api.gorbushka.market` and
`api.store1.example.com`. The workflow deliberately stops before release
activation if DNS, certificate issuance, nginx validation, or the JSON
`/bootstrap` check fails.
### 3.5 Deploy

View File

@@ -8,6 +8,10 @@ Single entry point for a backend developer picking this up cold. Written 2026-08
## 1a. Multi-tenancy — the thing that shapes every endpoint
The final executable infrastructure/backend contract is
[TENANT-API-DOMAIN-HANDOFF.md](TENANT-API-DOMAIN-HANDOFF.md). Follow it for
hostname normalization, CORS, nginx, TLS, CI secrets, and acceptance checks.
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 complete current browser hostname and protocol (localhost still uses the development proxy).
@@ -87,7 +91,12 @@ npm install # pulls @marketplaces/auth over git, no credentials needed
npm run build # -> dist/dexarmarket
```
Angular 22, Node 20+. nginx serves `/srv/marketplaces/current/frontend`, so deploying means copying `dist/dexarmarket` there — **manually, today.** There is no CD pipeline. Because of the multi-tenant design (§1a), one such deploy updates every domain at once.
Angular 22, Node 24+. nginx serves `/srv/marketplaces/current/frontend`.
[`deploy.yml`](../../.github/workflows/deploy.yml) builds and atomically deploys
pushes to `main`; one deployment updates every domain at once. Before activation,
the workflow reconciles TLS, exact CORS, and reverse proxying for every host in
`STOREFRONT_DOMAINS`. Production deployment requires the documented CI secrets
and the one-time [`server-setup.sh`](../../scripts/deploy/server-setup.sh) run.
## 7. Known open decisions

View File

@@ -2,6 +2,8 @@
> **New here? Start with [BACKEND-HANDOFF.md](BACKEND-HANDOFF.md)** — reading order, current infrastructure state, auth surface, and what a working dev environment still needs.
>
> **Implementing tenant routing/nginx? [TENANT-API-DOMAIN-HANDOFF.md](TENANT-API-DOMAIN-HANDOFF.md)** is the final host normalization, CORS, TLS, reverse-proxy, CI-secret, and acceptance contract.
>
> **Want every endpoint in one place? [FRONTEND-API-SURFACE-COMPLETE.md](FRONTEND-API-SURFACE-COMPLETE.md)** — the final handoff doc. Generated directly from source, all 90 endpoints the frontend currently calls plus 3 response-shape additions on existing endpoints, marked Specified / Inferred / Undocumented against the contracts below. Use it to see gaps across all contracts at once; use the individual Phase/Track docs for full entity shapes and invariants.
This directory is the complete set of wire contracts for building the backend behind [Product Plan v3.1](../PRODUCT-PLAN-v3.1-GAP-ANALYSIS.md). Each doc specifies entities, endpoints, and invariants only — never DB schema or service boundaries, which stay backend's own call.

View File

@@ -0,0 +1,124 @@
# 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:
```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.<storefront-host>`, allow exactly:
```http
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:
```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.<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.