Status: canonical. This is the single authoritative backend/API contract for this Angular multi-tenant marketplace platform. It supersedes and replaces `docs/BACKEND.md`, `docs/BACKEND_API.md` (old), `docs/BACKEND-INTEGRATION-PROMPT.md`, `docs/BACKEND-DIFF-VS-MAIN.md`, `docs/architecture/backend/Backend-Platform-API-Spec.md`, and the API-contract portions of `docs/BOOTSTRAP.md`. Those files are deleted; this document is where all of that content now lives, verified against the current source tree on branch `B2B`.
Audience: a backend engineer implementing this platform's API with no other context, and any frontend engineer who needs the ground truth for what the client sends and expects.
## How to read this document
Every endpoint, DTO, and behavior below is tagged with a status:
- **CURRENT** — real HTTP call today, verified in code (file path cited). Shape is frozen; do not change it.
- **PLANNED** — declared in the bootstrap model (`apiEndpoints.builder` / `apiEndpoints.backoffice`) or implied by a gateway interface, but not wired to real HTTP yet. Served today by a `*LocalGateway` (in-memory/localStorage/IndexedDB mock) bound via an Angular DI token. Implementing the real endpoint means writing one `*ApiGateway` class against the documented interface and rebinding the token — no UI change.
- **FUTURE** — reserved contract only. Nothing exists client-side yet beyond a placeholder or a "coming soon" page. Speculative shape, subject to change once real requirements exist.
Never mix these tags. If you can't find a field in the cited model file, it isn't real — ask rather than invent (see Assumptions below for the few places a judgment call was necessary).
## Assumptions (only where the source code was genuinely ambiguous)
1.**Bootstrap path.** Code call sites use `GET /bootstrap` relative to the resolved API base (`ApiConfigService.getBaseUrl()`), not `/api/v1/bootstrap`. This document uses `GET /bootstrap` throughout, consistent with `docs/architecture/backend/Backend-Platform-API-Spec.md`'s draft and every existing doc. If a backend team versions it, `GET /v1/bootstrap` is a compatible evolution — do not silently rename without a schema/version bump story (see §5).
2.**`builder/*` and `backoffice/*` path prefixes.** No literal string constant for these prefixes exists in code (the mock gateways never build a URL at all — they're pure in-memory/localStorage). The prefixes used throughout §6 follow the naming already established in `docs/BACKEND.md`/`docs/BACKEND-INTEGRATION-PROMPT.md` and match `ApiEndpointsConfig`'s `builder`/`backoffice` buckets (`src/app/shared/models/config/api-endpoints.model.ts`). Treat every concrete path in §6.7–§6.16 as a **PLANNED contract proposal**, not a verified literal string — the *shapes* (request/response bodies, field names, enums) are verified against the real TypeScript interfaces; the *paths* are a reasonable default you're free to adjust as long as `apiEndpoints.backoffice`/`apiEndpoints.builder` in the published bootstrap stays in sync with whatever you choose.
3.**Auth/payment endpoints are explicitly frozen** by ADR-010 and out of scope for change — documented here for completeness (§2, §6.2) but treat as read-only reference, not a request for redesign.
This is not a single marketplace — it is a multi-tenant platform. One Angular codebase serves unlimited tenants ("marketplaces"), each configured entirely at runtime from a backend-served `BootstrapConfig` document. No marketplace-specific code exists in the frontend (`docs/context/adrs/ADR-0001-marketplace-platform-vision.md`).
Every tenant has three logical surfaces sharing one domain:
- **Website** — the public storefront (catalog, product, cart, checkout-adjacent flows).
- **Builder** (Project Editor) — the tenant admin's visual editor for the same `BootstrapConfig` the storefront renders from. No parallel model.
Website --> API["Storefront REST API (category/items/search/cart/reviews)"]
Builder --> BuilderAPI["Builder REST API (draft/publish)"]
Backoffice --> BackofficeAPI["Backoffice REST API (per-domain CRUD)"]
API --> DB[("Database")]
BuilderAPI --> DB
BackofficeAPI --> DB
```
### Request flow (every storefront call)
1. Application code calls `this.http.get('/api/category')` or builds an absolute URL via `ApiConfigService`.
2.**`apiBaseUrlInterceptor`** (`src/app/interceptors/api-base-url.interceptor.ts`) rewrites any `/api/...` URL to the resolved tenant base via `ApiConfigService.toApiUrl()`.
3.**`apiHeadersInterceptor`** (`src/app/interceptors/api-headers.interceptor.ts`) attaches `X-Region`, `X-Language`, `Currency`, `WebSessionID` to every request whose URL resolves as an API request (`ApiConfigService.isApiRequest()`).
4.**`adminAuthHeadersInterceptor`** (`src/app/core/admin-auth/admin-auth-headers.interceptor.ts`) additionally attaches `AdminWebSessionID` and, if present, `Authorization: Bearer <token>` — but only when the URL contains `/admin/`.
5.**`cacheInterceptor`** (`src/app/interceptors/cache.interceptor.ts`) short-circuits GET requests to `/category`, `/category/:id`, `/items/:id` against an in-memory TTL cache (see §13).
6.**`mockDataInterceptor`** (`src/app/interceptors/mock-data.interceptor.ts` / `.production.ts`) — dev-only; when `environment.useMockData` is true, intercepts before any of the above and serves fixture JSON. Not part of the real contract; ignore for backend implementation.
7. Request reaches the resolved absolute base (see base-URL resolution below).
Interceptor registration order (as configured, `mockData → apiBaseUrl → apiHeaders → adminAuth → cache`) matters: base-URL rewriting must happen before headers are attached (headers only attach to recognized API requests), and admin headers only attach to `/admin/` URLs so customer requests never carry an `AdminWebSessionID`.
3.**`environment.tenantApiTemplate`** — host-pattern template, e.g. `https://{tenant}.api.dexarmarket.ru:445`.
4.**Bootstrap override** (opt-in only) — `bootstrap.apiEndpoints.website.baseUrl` or `bootstrap.tenant.apiBaseUrl`, used only when `environment.allowBootstrapApiOverride === true`**and** the value is an absolute `http(s)://` URL. Disabled by default in both `environment.ts` and `environment.production.ts`.
5.**Fallback** → `environment.apiUrl`.
Tenant identity itself comes from `TenantResolverService` (host-based), never a path or query parameter — the backend must resolve tenant from the `Host` header the same way, before authorization and routing (mirrors `docs/architecture/backend/Backend-Platform-API-Spec.md` §1).
**Backend impact for a new tenant:** either register it in `tenantApiBaseUrls`, or serve it at the `tenantApiTemplate` host pattern — no frontend rebuild required if the template host pattern holds.
### Layered feature architecture (why the UI never touches an endpoint directly)
Every admin domain (categories, products, orders, transactions, users, monitoring, dashboard) follows: `*Gateway` interface → `*LocalGateway` (mock, bound today) / `*ApiGateway` (real, to be built) → DI token → Facade → Page components. See §14 for the full replacement mechanics. This is why implementing a backend for any one domain is additive: write one class, rebind one token, ship.
---
## 2. Authentication
**Scope note (ADR-010):** authentication, payment, and authorization *behavior and contracts* are frozen — this section documents the existing contract for completeness and integration correctness. It is not a request to redesign the auth flow.
### 2.1 Why Telegram, why sessions, why not just JWT-on-login
The platform authenticates end users (both customers and admins) via **Telegram**, not a password. A user scans a QR code (desktop) or taps a deep link (mobile) that opens the platform's Telegram bot with a `start` payload equal to a server-generated session id. The bot, on the backend side, associates that Telegram identity with the session id. The frontend never sees Telegram credentials — it only polls "is this session id now associated with a logged-in Telegram user?" This is why the flow is **session-first**: the session id exists *before* the user is authenticated, and authentication is something that happens to an already-existing session, not something that produces one.
**Why not a JWT immediately on session creation:** the session id is created anonymously (no identity yet) so the frontend has something to encode into a QR code before any identity exists. A JWT would need a subject; there isn't one until the Telegram bot round-trip completes.
**Why sessions (cookie) rather than only a bearer token in memory:** the customer and admin session ids are stored as cookies (`webSessionID`, `adminSessionID`) specifically so a page refresh or new tab doesn't lose login state — a pure in-memory token would not survive navigation. See §2.6 for exact cookie policy.
**Why Telegram alone isn't enough for admin:** completing the Telegram QR flow only proves "this is a real Telegram account holder." It proves nothing about whether that account is *authorized* to act as a marketplace admin. See §2.5 — this is the platform's most significant open security gap today.
### 2.2 The one session API (CURRENT)
Both customer and admin login are, today, **the same backend endpoint**: `{authApiUrl}/users/sessions` (`environment.authApiUrl`, e.g. `https://users.vitanova.network:456`), called exclusively through `TelegramSessionApiService` (`src/app/services/telegram-session-api.service.ts`). There is no separate admin session endpoint.
| Method | Endpoint | Purpose | Caller |
|---|---|---|---|
| `POST` | `{authApiUrl}/users/sessions` | Create an anonymous session, get the Telegram bot deep-link | `TelegramSessionApiService.createSession()` |
| `GET` | `{authApiUrl}/users/sessions/:webSessionID` | Poll session status (has the Telegram bot round-trip completed?) | `TelegramSessionApiService.checkSessionOnce()` |
**`POST /users/sessions`** — request body `{ webSessionID: string }` (a client-generated GUID, `generateGuid()` from `src/app/shared/util/guid.util.ts`), header `WebSessionID: <same guid>`. Response is normalized defensively (`normalizeWebSession`) against many possible field-name variants — the client tolerates `webSessionID`/`WebSessionID`/`webSessionId`/`sessionID`/`SessionID`/`sessionId`/`id`/`ID` for the session id field, and similar variant lists for `user`, `status`/`active`/`loggedIn`, `username`, `firstName`/`first_name`, `expiresAt`/`expires`. **Backend implementers: pick one canonical field name per concept; the client's tolerance is defensive, not a suggestion to send multiple.**
The frontend generates the QR/deep-link URL itself (not from a backend field): `https://t.me/{telegramBot}?start={webSessionID}` for the QR/browser flow, `tg://resolve?domain={telegramBot}&start={webSessionID}` for the in-app deep link. `telegramBot` is a frontend environment constant (`environment.telegramBot`, e.g. `myAMLKYCBOT` / `DexarSupport_bot`), not backend-supplied.
**`GET /users/sessions/:webSessionID`** — polled every 5 seconds (`POLL_INTERVAL_MS`, `QrLoginEngine`, `src/app/shared/qr-login/qr-login.engine.ts`), up to 100 polls (`MAX_POLLS`) before the client gives up and shows `expired`. Also re-checked on `visibilitychange`/`focus`/`pageshow` events (the "user returned from the Telegram app" recovery path — `openAppLogin()` navigates away via `window.location.href`, so the SPA needs to re-verify session state whenever the tab regains focus). Response shape normalizes to:
If the backend response omits an expiry, the client defaults to `now + 3600s` (`SESSION_MAX_AGE_SECONDS = 60 * 60`) — this is a client-side fallback only; the backend should always send a real `expiresAt`.
**`DELETE /users/sessions/:webSessionID`** — best-effort; client clears local state regardless of response (`catchError(() => of(null))`), so a network failure on logout never blocks the UI from appearing logged out.
### 2.3 Customer login (CURRENT)
`AuthService` (`src/app/services/auth.service.ts`) wraps `TelegramSessionApiService` and owns:
- Cookie `webSessionID`, `SameSite=Lax`, `Max-Age=3600`, `Secure` when served over HTTPS, `Path=/`.
- Auto session-refresh: schedules a re-check 60 seconds before `expires` (minimum 30s out), so an active tab silently re-validates before expiry rather than surprising the user with a sudden logout.
- On app init, always calls `checkSession()` once against whatever cookie is present.
### 2.4 Admin login (CURRENT, same backend as customer)
`AdminAuthService` (`src/app/core/admin-auth/admin-auth.service.ts`) is a **structurally identical** wrapper around the same `TelegramSessionApiService` — same `createSession`/`checkSessionOnce`/`logout` calls, same polling engine (`QrLoginEngine`) — with only the *storage* kept separate:
| Header interceptor | `apiHeadersInterceptor` sets `WebSessionID` | `adminAuthHeadersInterceptor` sets `AdminWebSessionID` (+ `Authorization: Bearer` if a token exists) |
**`AdminAuthService.getAdminToken()`/`setAdminTokens()`/`clearAdminTokens()`** exist and are wired into the `Authorization` header, but nothing currently calls `setAdminTokens()` — this is dead-but-ready plumbing for once the backend issues a real JWT access/refresh pair on top of the Telegram session. Treat this as a **PLANNED** extension point, not a current contract.
**Dev-only bypass:** `AdminAuthService.devBypassLogin()` fabricates a local session (`dev-bypass-<timestamp>`) and activates it directly, skipping the QR flow — but is a runtime no-op when `environment.production === true` (checked at call time, not just build time). Reachable via `?devBypassAdmin=true` per `docs/BACKEND-DIFF-VS-MAIN.md`. Not a backend concern beyond knowing it exists — it never calls any endpoint.
### 2.5 The admin authorization gap (CRITICAL — security-relevant, unresolved)
Because admin and customer login hit the **identical**`POST /users/sessions` endpoint, **the backend currently has no way to know, at the moment the QR is scanned, that this is an admin login attempt versus a customer one.** The frontend's only decision is *where to store* the resulting session id (`adminSessionID` cookie vs. `webSessionID` cookie) — it cannot and does not decide, and cannot enforce, whether the Telegram user who completed the scan is actually authorized to act as an admin.
**Concretely: any Telegram user who completes the QR flow while the admin login screen happens to be showing receives a valid `adminSessionID`.** There is currently no server-side check that rejects a non-admin Telegram user's session when it's used with `AdminWebSessionID`.
**Required fix (server-side, not fixable from the frontend):** when an API call arrives carrying `AdminWebSessionID`, the backend must look up whether that Telegram user id is in the admin/role registry and reject (401/403) if not. This is a hard requirement before this system is production-safe for any tenant with real admin/customer separation.
**Also prepared, not wired:** `Ed25519VerificationService` (`src/app/core/admin-auth/ed25519-verification.model.ts`) defines a `requestChallenge()`/`verify()` contract for a future non-Telegram, challenge/signature-based admin auth path (`Ed25519Challenge { nonce, timestamp, payload }` → `Ed25519SignedResponse { challenge, publicKey, signature }` → `Ed25519VerificationResult { valid, reason? }`). The current binding is `NoopEd25519VerificationService`, which **throws** rather than silently accepting anything — a deliberate fail-closed placeholder, safe to leave wired until a real challenge/verify endpoint exists. **FUTURE** — no endpoint exists for this today.
### 2.6 Cookies, headers, and session identity summary
| Concern | Customer | Admin |
|---|---|---|
| Cookie | `webSessionID` | `adminSessionID` |
| `SameSite` | `Lax` | `Strict` |
| `Secure` | yes, when `https:` | yes, when `https:` |
| `Max-Age` | 3600s | 3600s |
| Header sent on API calls | `WebSessionID` (all API requests) | `AdminWebSessionID` (only `/admin/` requests) + optional `Authorization: Bearer <adminToken>` |
| Anonymous fallback | 32-char hex, `localStorage: web_session_id`, generated client-side if no session exists | none — unauthenticated admin routes redirect to login |
### 2.7 Refresh, expiry, unauthorized flows
- **Refresh:** not a token-refresh in the OAuth sense — it's a periodic re-poll of `GET /users/sessions/:id` scheduled ~60s before the session's `expires` timestamp. If still active, the client silently re-activates the session (same cookie, updated in-memory signal); if not, the client transitions to `unauthenticated` and clears the cookie.
- **Expired token flow:** `checkSessionOnce` returning `active: false` (or erroring) clears all local auth state (`clearAuthState('unauthenticated')`) — cookie removed, signals reset, refresh timer cancelled. No automatic re-login; the user must re-scan.
- **Unauthorized flow (admin):** `adminAuthGuard` blocks navigation and calls `requestLogin()` (shows the login dialog) rather than a hard redirect — consistent with this being an SPA-level gate, not a route change.
### 2.8 Payments (frozen, documented for completeness)
`ApiService` (`src/app/services/api.service.ts`) also exposes the payment surface — **frozen per ADR-010**, not to be changed:
`QrCreateRequest`/`QrCreateResponse`/`CartPaymentRequest`/`QrDynamicStatusResponse` interfaces are in `api.service.ts` lines 10–66 — reproduced in §7.9. The response normalizer tolerates many field-name casings (`qrId`/`qrID`, `nspkID`/`nspkId`, etc.) for the same reason as session responses: multiple backend versions/typos have shipped over time and the client absorbs them rather than breaking.
---
## 3. Security
### 3.1 Origin is not authentication
The frontend sends no CSRF token and relies on cookies (`webSessionID`/`adminSessionID`, `SameSite=Lax`/`Strict`) plus header-based session ids (`WebSessionID`, `AdminWebSessionID`) as the actual authorization signal — **`Origin`/`Referer` headers must never be treated as an authentication mechanism.** They are trivially spoofable by any non-browser client (curl, a compromised browser extension, a malicious script running same-origin) and provide no proof of user identity or intent. Legitimate uses of `Origin` are limited to: (a) CORS allow-listing for browser-enforced preflight, and (b) defense-in-depth logging/anomaly detection — never as a substitute for verifying `WebSessionID`/`AdminWebSessionID` against a real, backend-held session record.
### 3.2 CORS
Not configured in this repository (no `nginx.conf` CORS block for the actual API hosts `api.dexarmarket.ru:445` / `users.vitanova.network:456` — those are separate servers, not part of this repo). Recommended production posture:
- Allow-list exact tenant storefront origins (from the tenant registry, not `*`).
-`Access-Control-Allow-Credentials: true` (cookies are in play).
- Preflight `OPTIONS` handling for any endpoint that receives custom headers (`X-Region`, `X-Language`, `Currency`, `WebSessionID`, `AdminWebSessionID`).
### 3.3 Endpoint authorization tiers
| Tier | Examples | Requirement |
|---|---|---|
| Public | `GET /bootstrap`, `GET /category`, `GET /items/:id`, `GET /searchitems` | No auth. Never leak per-user/per-session data through these. |
| Session-required (customer) | `POST /websession/:id` (cart), `POST /items/:id/callback` (review), `POST /purchase-email` | Valid `WebSessionID`; anonymous sessions are allowed (cart works for guests). |
| JWT/admin-session-required | `builder/*`, `backoffice/*` | Valid `AdminWebSessionID`**and** (§2.5) server-side verification that the underlying Telegram identity is actually provisioned as an admin. |
| Admin-role-required (finer-grained) | e.g. `users`/`roles` write endpoints vs. read-only `products` list | `AdminUser.roleId` → `AdminRole.permissions[]` (see §7.6) should gate specific mutations once a real permission model exists server-side. `PermissionsConfig` (`src/app/shared/models/config/permissions.model.ts`) is the bootstrap-level skeleton for this (`definitions: PermissionDefinition[]`, `roles: RolePermissions[]`) but is not yet enforced anywhere in the frontend beyond existing.
### 3.4 Rate limiting, replay protection, CSRF
None of these exist today, client- or server-side, for this platform. Recommendations for a production backend:
- **Rate limiting:** at minimum on `POST /users/sessions` (session creation is unauthenticated and cheap to hammer) and on write endpoints (`builder/bootstrap/publish`, any `backoffice/*` mutation).
- **Replay protection:** session ids are long-lived (1 hour) bearer-equivalent values sent in a custom header — treat header leakage (e.g. via logs, proxies) as a real risk; short expiry + refresh (already the pattern, §2.7) is the primary mitigation.
- **CSRF:** cookies are `SameSite=Lax` (customer) / `Strict` (admin), which meaningfully reduces cross-site request risk for the cookie itself, but the actual authorization value doubles as a **header** (`WebSessionID`/`AdminWebSessionID`) read from the cookie by JS and reattached — a state-changing endpoint that only checks the header, not the cookie, reopens CSRF risk from any origin that can trick a script into replaying a known session id. Recommend the backend validate that the header value matches a cookie-bound session, not just "does this header value exist in the sessions table."
### 3.5 Bearer tokens, HTTPS, headers
-`Authorization: Bearer <adminToken>` is sent when `AdminAuthService.getAdminToken()` returns non-null (currently always null in practice — see §2.4). Once real JWTs are issued, use standard `Authorization: Bearer` semantics; don't invent a custom scheme.
- Every environment config uses `https://` for `apiUrl`/`authApiUrl`/`qrApiUrl` in production; only localhost dev proxies over `/api`. HTTPS is mandatory in production — cookies rely on `Secure` being meaningful.
- Custom headers the backend must read: `X-Region`, `X-Language`, `Currency`, `WebSessionID` (all requests); `AdminWebSessionID`, `Authorization` (admin requests only).
### 3.6 Recommended production security checklist
- Enforce admin authorization server-side per §2.5 — this is the single highest-priority item in this entire document.
- Validate `WebSessionID`/`AdminWebSessionID` against a real backend-held session, never trust the header value alone as proof.
- Rate-limit session creation and all write endpoints.
- Serve `GET /bootstrap` with no secrets, ever (see §4) — it is public by design.
- Populate `apiEndpoints.builder`/`apiEndpoints.backoffice` in bootstrap only with paths that are actually protected server-side; publishing an endpoint in bootstrap is not itself a security boundary.
- CORS allow-list per tenant host, not wildcard, when credentials are involved.
---
## 4. Bootstrap
### 4.1 Endpoint
**CURRENT** — `GET /bootstrap`, resolved against the tenant API base (§1). No query params or body. Tenant resolved server-side from the `Host` header — **the frontend never sends a tenant id.** Loaded once at app startup by `ConfigService.loadBootstrap()` (`src/app/core/config/config.service.ts`) and cached via `shareReplay(1)` — repeat calls to `loadBootstrap()` within the same session return the cached observable unless `forceRefresh: true` is passed.
On localhost (or when `environment.useMockData === true`), `MockBootstrapProvider` serves `src/assets/mock/bootstrap/bootstrap.json` instead of a real HTTP call, so the app runs fully offline (`npm run dexar`). This mock file is the **canonical field-for-field reference payload** — match it exactly to avoid client-side normalization surprises. `ApiBootstrapProvider` is the real-HTTP implementation, both behind `CONFIG_PROVIDER` DI token per ADR-004.
### 4.2 Response caching
No explicit `ETag`/`304` handling exists in the frontend today — the entire response is cached client-side for the session lifetime via `shareReplay(1)`. A backend implementer is free to add `ETag`/`Cache-Control` (recommended, see §13) since the client doesn't currently send conditional-request headers for this endpoint — adding them is additive and safe.
| `featureFlags` | `feature-flags.model.ts` | Boolean map with 14 named keys (`wishlist`, `compare`, `reviews`, `questions`, `comments`, `recommendations`, `blog`, `chat`, `analytics`, `notifications`, `coupons`, `loyalty`, `giftCards`, `invoices`) plus an open `[key: string]: boolean` index — new flags can be added without a model change. |
| `features` | `features-config.model.ts` | Newer, more granular `MarketplaceFeaturesConfig` (16 booleans: wishlist/compare/reviews/comments/questions/recommendations/recentlyViewed/searchHistory/recentlySearched/ratings/share/brands/manufacturers/availability/discounts/badges). Resolvers fall back to `featureFlags`/`productPage`/`userExperience`/`catalog` for older bootstraps lacking this field — **do not remove `featureFlags` when adding `features`; both must be sent for backward compatibility until every consumer is confirmed on the new surface.** |
| `apiEndpoints` | `api-endpoints.model.ts` | `{ bootstrap: ApiEndpointConfig, website: Record<string, ApiEndpointConfig>, builder: Record<string, ApiEndpointConfig>, backoffice: Record<string, ApiEndpointConfig> }` where `ApiEndpointConfig = { path, method, timeoutMs? }`. **Never contains secrets.** Populate `builder`/`backoffice` buckets as you implement each endpoint so the frontend can (eventually) discover them. |
| `navigation` | `navigation.model.ts` | `header: NavigationItemConfig[]`, `footer: NavigationItemConfig[] \| FooterNavigationGroupConfig[]` (two accepted shapes — flat list or grouped), optional `sidebar[]`. Each item: `id`, `label` (string or `{[locale]: string}`), `route?`, `type?`, `key?` (e.g. `'staticPage'` + a page id — see §12), `icon?`, `order?`, `visible?`, `visibleWhenFlags?: string[]`, `children?`. |
| `footer` | `footer-config.model.ts` | `logoUrl?`, `paymentIcons?: {src, alt, width?, height?}[]`, `copyrightText?` (string or per-locale), `columns?: FooterColumnConfig[]` (`{id, title, links: {id,label,pageKey?,url?}[]}`), `socialLinks?: {id,label,url,icon?}[]`. `legalPageKeys`/`staticPageKeys` are **deprecated**, superseded by `columns` — still read for already-saved configs, do not emit them for new tenants. |
| `productPage` | `product-page-config.model.ts` | Feature config only for PDP — `rating`, `reviews` (`pageSize`, `showSummary`, `mode: pages\|load-more`), `questions` (`pageSize`, `allowSubmission`), `tabs` (`items[]` from a fixed enum), `relatedProducts`, `actions` (`addToCart`,`buyNow`,`wishlist`,`compare`,`share`,`notifyMe`). No review/question *data* here. |
| `userExperience` | `user-experience-config.model.ts` | Feature config only — `wishlist` (`headerBadgeEnabled`), `compare` (`maxItems`, `hideIdenticalDefault`, `highlightDifferencesDefault`), `recentlyViewed` (`maxItems`, `widgetEnabled`), `share`, `continueBrowsing`, `savedSearches` (`maxItems`). Never user-specific lists — those are client-local today (§6.13). |
**Resolution rule:** a static page resolves on the storefront only when `enabled === true`**and**`status === 'published'` — independent of whether the surrounding bootstrap has been published. A page with `status: 'draft'` stays invisible even after the whole tenant config is published. Compatibility default when normalizing legacy data (no `enabled`/`status` present): `enabled: true, status: 'published'` — existing tenants are never silently un-published.
**Known data inconsistency:** the model requires `slug`, but the reference mock (`src/assets/mock/bootstrap/bootstrap.json`) leaves `slug` empty for some pages and only populates `route` (e.g. `/about-us`). The frontend's duplicate-detection falls back to `route` when `slug` is empty. **Decide and standardize:** either always populate `slug` server-side, or formally make `slug` derivable from `route` — do not leave both optional indefinitely.
### 4.8 Lifecycle: how the frontend builds itself from it
1.`ConfigService.loadBootstrap()` fetches (or, mock mode, reads local JSON) and parses `BootstrapConfig`. Cached via `shareReplay(1)`.
2.`PlatformRuntimeService` applies theme tokens as CSS variables, sets branding, and exposes parsed pages/navigation/footer/static-pages to the rest of the app.
3. The Section Engine / Widget Host render pages from `bootstrap.pages` on route match.
4.`PlatformRuntimeService.reloadFromBootstrap(next)` re-applies an entire new `BootstrapConfig` in-memory without a full page reload — this is what the Project Editor's Publish/Preview actions use (§6.7).
The Project Editor edits an **in-memory copy of this exact same `BootstrapConfig`** — there is no parallel editor model or DTO translation layer (ADR-0001's core rule).
---
## 5. API conventions
### 5.1 Versioning
No path-based versioning exists today (`/bootstrap`, not `/v1/bootstrap`) — `schemaVersion` inside the bootstrap payload is the actual contract version signal. Recommendation for new `builder/*`/`backoffice/*` endpoints: keep them unversioned in the path (matching existing convention) and rely on additive-only changes (new optional fields) within a schema-version line; bump `schemaVersion`'s major segment for breaking changes and coordinate a frontend release.
### 5.2 Naming and pluralization
Existing storefront endpoints are **inconsistently pluralized** by historical accident (`/category` singular-collection, `/items/:id` plural-collection, `/searchitems` compound) — **do not "fix" this**, it's a frozen contract (§6.1). For all **new**`builder/*`/`backoffice/*` endpoints, use plural resource collections (`/backoffice/products`, `/backoffice/orders`) consistently — this document's proposed paths in §6.7–§6.16 already follow that convention.
### 5.3 Status codes
| Code | Meaning | Used for |
|---|---|---|
| 200 | Success | GET, successful PUT/PATCH/POST that returns a body |
| 201 | Created | POST that creates a resource (e.g. `POST /backoffice/products`) |
| 204 | No Content | DELETE, or POST/PUT with no meaningful response body |
| 400 | Bad Request | Malformed request (not a validation failure — see 422) |
| 401 | Unauthorized | Missing/invalid session |
| 403 | Forbidden | Valid session, insufficient role/permission (including the admin-authorization gap fix, §2.5) |
- **Page-based** (admin lists): `page`/`pageSize` in filter objects (`AdminProductListFilters`, `AdminOrderListFilters`, `AdminReviewListFilters`, `AdminTransactionListFilters` all carry `page: number; pageSize: number`), response shape `{ items: T[], total: number, page: number, pageSize: number }` (`AdminProductsListResult`, `AdminOrdersListResult`, etc. — this exact 4-field envelope repeats across every paginated admin list; treat it as the standard admin list-response envelope for any new domain too).
- Admin product list sort (`AdminProductSort`): `title|price|priority|stock|updated`.
- Every admin `*ListFilters` interface has a free-text `search: string` — treat it as a substring match across the domain's primary human-readable fields (name/title/email/order number as applicable) unless a domain-specific note says otherwise.
### 5.6 PATCH vs PUT, DELETE semantics
- **PUT** — full-resource replace or explicit "save current state" (`PUT /builder/bootstrap/draft` — whole document).
- **PATCH** — partial update by convention for status-only or single-field admin mutations (`updateStatus`, `setUserRole`, `setReviewVisible`, etc. — even though the gateway interfaces are transport-agnostic, model these as PATCH since they touch one field/aspect, not the whole entity).
- **DELETE** — categories/products/orders all expose an explicit `delete*` method returning `Observable<void>` (→ `204`), **separate from**`archive*`/`restore*` (soft delete — see below). Reviews (`deleteReview`) and orders (`deleteOrder`) also have hard-delete methods distinct from any archive flow — confirm with product/legal whether hard delete should really be permitted for orders (audit-trail implications) before implementing it as literal data removal versus a tombstone.
### 5.7 Soft delete
Present as a first-class concept in two domains today:
No universal convention between `deletedAt: string|null` and `archived: boolean` — both are real, keep them domain-consistent with what's documented in §7 rather than unifying speculatively.
### 5.8 Idempotency
Not addressed anywhere in the current frontend contract. Recommend `Idempotency-Key` header support on `POST /builder/bootstrap/publish` and any payment-adjacent endpoint at minimum, since a retried publish/payment call must not double-apply.
### 5.9 Response/error format
See §11 for the single standard error envelope. Success responses have no forced envelope — most endpoints return the resource (or resource array) directly, not wrapped in `{ success, data }`. Follow the shapes documented per-endpoint in §6, not a generic wrapper.
### 5.10 Dates, timezone, UUID
- All timestamps in existing DTOs are ISO 8601 strings (`createdAt`, `updatedAt`, `expiresAt`, `generatedAt`, etc.) — never epoch numbers, except `AdminDashboardActivityEntry.timestamp: number` and `AdminMonitoringEvent`-adjacent internal caching (`Date.now()`), which are frontend-local, not wire-format precedent.
-`TenantConfig.timezone` (IANA string, e.g. `Europe/Moscow`) exists but nothing currently converts times server-side based on it — treat display-timezone conversion as a frontend concern unless otherwise decided.
-`TenantConfig.id: UUID` — `UUID`/`UrlString` are branded string types (`src/app/shared/types/primitive.types.ts`), i.e. plain strings at the wire level; no special serialization.
### 5.11 LocaleMap and translatable fields
The platform-wide rule (ADR-0001): **all user-facing text is translatable via a `translations.{lang}` shape, never a flat hardcoded field.** This repeats across the codebase as `Record<string, T>` keyed by locale code (`ru`/`en`/`hy` in this deployment, but the model itself is not hardcoded to those three): `AdminProduct.translations: Record<string, AdminProductTranslation>`, `AdminCategory.translations: Record<string, AdminCategoryTranslation>`, `StaticPageConfig.translations: Record<string, StaticPageTranslationConfig>`, `NavigationItemConfig.label: string | Record<string, string>`, `FooterConfig.copyrightText: string | Record<string, string>`. When adding/removing a supported locale, every translatable object must expose/drop that locale's key — generically, never per-field hardcoding.
### 5.12 Enums
Enums throughout this document are TypeScript string-literal unions, not numeric — send/return the literal string values exactly as documented (e.g. `"processing"`, not `2`).
`ApiService` normalizes both legacy and "backOffice" response variants defensively (dual field names, `0x`-hex colours → CSS hex, `names[]` → `translations`, `imgs[]` → `photos`, tolerance for the literal Go-side typo `valuue`). **Do not "fix" these variant shapes on the backend without confirming the frontend normalizer is updated in lockstep** — the normalizer exists precisely because multiple backend response shapes have shipped historically.
| `getItem(id)` | `GET /items/:id` | Rating/reviews/questions derived from the item payload itself (embedded `comments`/`questions`), not a separate call. Cached client-side. |
| `submitQuestion({...})` | `POST /items/:id/questiion` | `{question, sessionID, timestamp}` | `{message: string}` — **note the literal path typo `questiion`, keep it; it is a frozen contract, not a bug to fix.** |
Current: in-memory products/categories/tags power autocomplete (`SearchAutocompleteService`); trending returns `null` (UI hides gracefully, never fabricates data); search history is `LocalSearchHistoryRepository` (guest, localStorage) with a `BackendSearchHistoryRepository`**placeholder class already defined but unimplemented** (`src/app/features/search/services/search-history.repository.ts` — both classes exist side by side, only the backend one is a no-op today).
| Method | Endpoint | Status |
|---|---|---|
| GET | `/search/suggestions?q={term}` | PLANNED |
| GET | `/catalog/filters?category={id}&q={term}` | PLANNED |
| GET/POST/DELETE | `/me/wishlist`, `/me/compare`, `/me/saved-searches`, `/me/recently-viewed` | PLANNED — only needed if authenticated cross-device sync of these lists is wanted; today they're 100% localStorage (`LocalUserExperienceRepository`, keys `marketplace.ux.wishlist` etc.) |
Swapping any of these live requires only replacing the repository implementation — facade/UI unaffected by design.
The Project Editor edits the same `BootstrapConfig` the storefront reads (§4.8). Today: **load** is real (`GET /bootstrap`), **save** is `localStorage` only (`ProjectEditorDraftStorageService`, key `projectEditor.draftBootstrap.v1`, scoped by `tenant.id` — reload in another browser/tab and it's gone), **publish** re-applies the config in-memory via `PlatformRuntimeService.reloadFromBootstrap()` and flips a local `status` flag — **no backend call happens today.**`ProjectEditorIoService` today only does `JSON.stringify`/`JSON.parse` for manual export/import, no HTTP.
| Method | Endpoint | Body | Response | Notes |
|---|---|---|---|---|
| GET | `/builder/bootstrap/draft` | — | `BootstrapConfig`, or 404/empty if none (draft = published) | Tenant resolved by host, no `projectId` param — one domain = one tenant = one draft = one published bootstrap. |
| PUT | `/builder/bootstrap/draft` | `BootstrapConfig` | 200, updated draft metadata | Persists the draft. **Does not** affect `GET /bootstrap` (storefront-facing) until publish. |
| POST | `/builder/bootstrap/publish` | `BootstrapConfig` (or none, if publish always promotes the stored draft) | 200 | Validates (§9.1), then makes this `BootstrapConfig` what `GET /bootstrap` returns. **Only** endpoint that affects the live storefront. |
| POST | `/builder/bootstrap/validate` | `BootstrapConfig` | `ProjectValidationIssue[]` (see §9.1 for the exact shape) | Optional but recommended — mirrors the client validator so the editor can preview server-side issues before attempting publish. |
**Mandatory:** server-side re-validation on publish, equivalent to `ProjectValidator` (§9.1) at minimum. The client validator is not a trust boundary — a malicious or stale client can bypass all client-side checks.
Frontend files that change once this exists: `features/project-editor/facade/project-editor.facade.ts` (replace local save/publish with these endpoints, same public method signatures), `services/project-editor-draft-storage.service.ts` (becomes a fallback/offline cache, not the primary store).
### 6.8 Builder — Content pages / CMS (PLANNED)
Static pages are today edited entirely client-side and written only into the in-memory/localStorage bootstrap draft (§6.7) — no dedicated backend exists.
| Method | Endpoint | Notes |
|---|---|---|
| GET | `/builder/content-pages` | List, same shape as `bootstrap.staticPages`. |
| PUT | `/builder/content-pages` | Replace the set. |
| POST | `/builder/content-pages/import` | Bulk import. |
| GET | `/builder/content-pages/export` | Bulk export. |
| POST | `/builder/content-pages/validate` | Server-side HTML/SEO validation (§9.5). |
Backend must also support **content moderation/validation on publish** (disallow dangerous tags/attributes) and **revision history** — the frontend only sanitizes at render time (`MarketplaceHtmlEditorComponent` emits raw HTML with no sanitization by design; sanitization is a storefront-render concern, not an authoring concern), never at authoring time.
### 6.9 Backoffice — Categories (PLANNED)
Interface: `AdminCategoriesGateway` (`src/app/features/admin/categories/services/admin-categories-gateway.interface.ts`). Today: `AdminCategoriesLocalGateway`, in-memory, seeded once from `BackofficeDataService.loadCategories()` — nothing persists across reload. `GET /category` (§6.1, storefront-facing) is unaffected and stays as-is.
| GET | `/backoffice/categories/slug-taken?slug=&excludingId=` | — | `{taken: boolean}` |
| PATCH | `/backoffice/categories/reorder` | `{id, order}[]` | `204` — bulk reorder endpoint, not in the gateway interface today but needed since `AdminCategory.order` exists and the editor drag-reorders |
| GET | `/backoffice/products/categories` | — | `AdminProductCategoryOption[]` (`{id, title}`) — **still wired to its own seed today, not `AdminCategoriesGateway`; unify when both backends exist.** |
| POST | `/backoffice/products` | `AdminProduct` | `201 AdminProduct` |
| PUT | `/backoffice/products/:id` | `AdminProduct` | `200 AdminProduct` |
DTO: see §7.2, including the production-matching flat-variant shape (`AdminProductVariant`) — this is the highest-fidelity DTO in this document; read §7.2 carefully before implementing.
### 6.11 Backoffice — Orders (PLANNED)
Interface: `AdminOrdersGateway`. Today: `AdminOrdersLocalGateway` fabricates 24 synthetic in-memory orders — **no real order data exists anywhere in this system today.** The dashboard's Orders/Revenue cards intentionally render `pending-backend` rather than reading from this mock — they are not wired to it on purpose (the mock is order-management UI scaffolding, not a metrics source).
| POST | `/backoffice/orders/:id/archive` | — | `200 AdminOrder` |
| POST | `/backoffice/orders/:id/restore` | — | `200 AdminOrder` |
| DELETE | `/backoffice/orders/:id` | — | `204` |
| GET | `/backoffice/orders/export.csv` | — | CSV — UI has a CSV-export action |
| GET | `/backoffice/orders/:id/invoice` | — | printable invoice — UI has a print-invoice action |
| GET | `/backoffice/dashboard/revenue-summary` | — | aggregation feeding the dashboard's currently-`pending-backend` Orders/Revenue cards |
DTO: see §7.3, state machine: §8.1.
### 6.12 Backoffice — Transactions (PLANNED)
Interface: `AdminTransactionsGateway`. Today: `AdminTransactionsLocalGateway` derives one synthetic transaction per seeded mock order — **no real payment/transaction data exists.**
| GET | `/backoffice/transactions?search=&status=&type=&page=&pageSize=` | — | `AdminTransactionsListResult` |
| POST | `/backoffice/transactions/:id/retry` | — | `200 AdminTransaction` — semantics depend on whatever the real payment provider supports for retry |
Interface: `AdminUsersGateway`. Today: `AdminUsersLocalGateway`, fully synthetic. **Passwordless login itself is real** (§2.4) — only the roles/permissions/invitations/multi-session-listing layer on top is mocked.
| GET | `/backoffice/invitations` | — | `AdminInvitation[]` |
| GET | `/backoffice/users/:id/sessions` | — | `AdminSession[]` — **must reflect real multi-device sessions**, unlike today's `AdminAuthService` which only knows the current browser's session |
| GET | `/backoffice/users/:id/audit` | — | `AdminUserAuditEntry[]` |
Interface: `AdminModerationGateway`. No `*LocalGateway` file distinct from the interface was found under a separate name in this pass beyond `admin-moderation-local.gateway.ts` — same pattern as everywhere else (mock today, swappable).
Interface: `AdminDashboardMetricsGateway`. Today: `AdminDashboardMetricsLocalGateway` composes `BackofficeDataService.loadCategories()/loadProducts()` client-side into counts; everything else on the dashboard (marketplace status, theme, languages, last publish/save, bootstrap version, active layout, enabled widgets, system health) is derived from `ProjectEditorFacade` state, not a metrics endpoint. Recent Activity is `AdminDashboardHistoryService`, `localStorage`-backed, scoped per tenant (`adminDashboard.activityHistory.v1`) — will never show another editor's activity.
```ts
interface AdminDashboardMetricsGateway {
loadMetrics(): Observable<AdminDashboardMetrics>; // today only { categoriesCount, productsCount }
}
```
| Method | Endpoint | Response |
|---|---|---|
| GET | `/backoffice/dashboard/summary` | Real-time counts and trend deltas — richer than today's `{categoriesCount, productsCount}` |
| GET | `/backoffice/dashboard/activity` | Real audit-log-backed activity feed, readable by multiple concurrent admin sessions (unlike today's per-browser localStorage) |
Interface: `AdminMonitoringGateway`. Health section reads real data (`AdminDashboardFacade.healthChecks`) already — unaffected. Everything else (audit/security/login/failed-login/API/error/warning event feed, queue depths, webhook deliveries) is synthetic — **no logging, queue, or webhook infrastructure exists anywhere in this system today.**
| GET | `/backoffice/monitoring/events?category=&search=` | `AdminMonitoringEvent[]` — real structured logging with a query API by category/level/actor/time-range |
| GET | `/backoffice/monitoring/queues` | `AdminQueue[]` — real queue introspection, once a job runner exists |
| GET | `/backoffice/monitoring/webhooks` | `AdminWebhookDelivery[]` — real webhook delivery tracking, once webhooks exist as a feature at all (§15) |
DTO: §7.9.
### 6.17 Backoffice — Analytics (mostly FUTURE — no data source)
`features/admin/analytics/` computes real revenue/orders/top-products aggregations from mock order data (§6.11), but visitor traffic, conversion funnels, and heatmaps have **zero data source anywhere in this system** — no analytics/tracking pipeline, no event collection. The page renders `pending-backend` badges rather than fabricated numbers; no gateway method exists for this yet, unlike every other mocked domain.
| Method | Endpoint | Status |
|---|---|---|
| GET | `/backoffice/analytics/summary` | PLANNED — `AdminAnalyticsSummary`, revenue/orders/customers real once orders are real |
| GET | `/backoffice/analytics/traffic` | FUTURE — needs an actual tracking pipeline |
| GET | `/backoffice/analytics/funnels` | FUTURE |
| GET | `/backoffice/analytics/heatmaps` | FUTURE |
DTO: §7.10.
### 6.18 Media (PLANNED — ADR-0002)
Domain model and contract are already spec'd in `docs/context/adrs/ADR-0002-media-manager-contract.md`; reproduced here as the canonical copy. `MediaRepository` (`src/app/core/media/media-repository.ts`) abstract class, two implementations selected via DI token: `MockMediaRepository` (IndexedDB — not localStorage, binary blobs need it) and `HttpMediaRepository` (to be built).
**Media never enters the Bootstrap model** — like products/orders/users, media assets are runtime admin data, not tenant configuration (ADR-0001). DTO: §7.11.
### 6.19 Sitemap (FUTURE — static baseline only today)
`public/sitemap.xml` currently lists only the statically-known top-level routes (home/catalog/search/wishlist/compare) for the default locale, referenced from `public/robots.txt`. This platform is multi-tenant and config-driven — supported locales, categories, products, static pages are all resolved at runtime per tenant, not enumerable from the frontend at build time. A real per-tenant sitemap covering `/:lang/product/:id`, `/:lang/catalog/:categoryId`, `/:lang/:staticPath` needs a build-time or server-side job reading the same per-tenant data source (categories/products/static pages) and regenerating/serving this file dynamically — not something the SPA can produce correctly on its own.
---
## 7. DTOs
Every interface below is lifted directly from its cited source file — field lists, optionality, and nullability are exact, not paraphrased.
Note vs. the storefront-facing `CategoryDto`/`CategoryCardConfig` (`src/app/features/admin/**` seed source): those lack `parentId`, `slug`, `icon`, `imageUrl`, `status`, `deletedAt`, `seo`, `translations` — the admin model is a superset. Reconciling the two into one backend category table is a real design decision for the backend team; this document only certifies what the frontend needs, not how storage should be normalized.
**Validation notes for the backend:** `sku` must be unique per tenant (§9.4). `variants[].sku` should also be unique among a product's own variants at minimum. `price`/`currency` required at the top level even when `variants` carries its own per-combination `prices` — top-level price is the "from" price shown on listing cards.
`AdminCustomer` (`src/app/features/admin/customers/models/admin-customer.model.ts`) is **not its own stored entity** — it's derived by grouping `AdminOrder` records by `customer.email`, since no dedicated customer gateway/backend exists:
If/when a real customer domain is built, this becomes a real GET endpoint rather than a client-side aggregation — every field here should map 1:1 to a real column/aggregate, never fabricated.
`pending-backend` is a real, intentional card status — it means "this sprint has no data source for this metric yet," rendered honestly instead of a fabricated number. Any new metrics endpoint should let the frontend distinguish "zero" from "no data source" the same way (e.g. omit the field, or use `null`, rather than sending `0`).
`conversionRate: number | null` and `retentionPercent: number | null` are the platform's explicit convention for "no data source yet" — `null` is a first-class, meaningful value here, not an oversight. Replicate this pattern for any new metric without a real source rather than defaulting to `0`.
### 7.11 Media — `src/app/core/media/models/media-asset.model.ts`
```ts
type MediaAssetKind = 'image' | 'svg' | 'pdf' | 'other';
### 7.12 Bootstrap DTOs — see §4.3–§4.6 for the complete `BootstrapConfig` tree (not repeated here to avoid duplication).
---
## 8. State machines
### 8.1 Orders — `AdminOrderStatus`
```
pending → processing → shipped → delivered
pending → cancelled
processing → cancelled
shipped → delivered
delivered → refunded (via payment.status refund_requested → refunded, not a direct status jump)
* → refunded (only through the refund request flow, never a direct status write)
```
Invalid transitions to reject server-side: `delivered → pending`, `cancelled → *` (terminal), `refunded → *` (terminal), skipping `shipped` to jump straight `processing → delivered` is allowed by the model (no `shipped` requirement enforced client-side) but should be a deliberate backend policy decision, not silently permitted. Every transition must append an `AdminOrderTimelineEntry {status, timestamp, note}` — the timeline is the audit trail, never overwritten.
### 8.2 Payment status — `AdminOrderPaymentStatus`
`stockStatus` (`in_stock|low_stock|out_of_stock`) is derived from `quantity`, not independently settable — treat it as backend-computed, never accept it as client input on create/update.
### 8.4 Reviews — `AdminReviewStatus`
```
pending → approved
pending → rejected
pending → spam
approved ⇄ rejected (moderator can reverse a decision)
* → spam (any state can be flagged spam)
```
Every status change should append an `AdminReviewTimelineEntry`. `visible`, `pinned`, `featured` are independent booleans layered on top of `status` — a `rejected` review should probably force `visible: false` server-side regardless of what the client sends (defense in depth), even though nothing in the current frontend enforces that coupling.
### 8.5 Reports — `AdminReportStatus`
```
open → resolved
open → dismissed
resolved ⇄ dismissed (reversible, e.g. re-opened as a mistake)
authenticated → unauthenticated (logout, or expiry check fails)
```
### 8.8 Users — `AdminUserStatus`
```
invited → active (invitation accepted)
active ⇄ suspended
invited → (invitation revoked, never becomes a user)
```
### 8.9 Invitations — `AdminInvitationStatus`
```
pending → accepted
pending → expired (time-based, backend job)
pending → revoked (admin action)
```
---
## 9. Validation
### 9.1 Backend vs. frontend validation — the client validator is not a trust boundary
`ProjectValidator` (`src/app/features/project-editor/services/project-validator.service.ts`) runs entirely client-side and blocks the Publish button, but **a malicious or stale client can bypass all of it.**`POST /builder/bootstrap/publish` must re-run equivalent checks server-side. The exact issue shape to mirror:
```ts
interface ProjectValidationIssue {
code: string; // stable machine code
message: string; // i18n key, not a rendered string
section?: string; // editor section for UI badges
fieldKey?: string; // schema field key for inline errors
Slug/route uniqueness, hex-color format, and URL format are validated **both** client-side (fast UX feedback) and must be re-validated server-side (real trust boundary) — this is intentional duplication, not a bug to eliminate on either side.
### 9.3 Required vs. generated fields
Generated (backend-set, never accept as client input on create): `id`, `createdAt`, `updatedAt`, `deletedAt`, `itemsCount` (categories), `stockStatus` (products, derived from `quantity`), order `timeline[]` entries, transaction `audit[]` entries, review `timeline[]` entries.
Required on create (reject with 422 if missing): category `title`/`slug`; product `name`/`sku`/`categoryId`/`price`/`currency`; order items must reference valid `productId`s.
### 9.4 Slug / SKU / barcode uniqueness
- Category `slug` — unique per tenant. `isSlugTaken(slug, excludingId)` gateway method exists precisely for this — mirror it as `GET /backoffice/categories/slug-taken`.
- Product `sku` — unique per tenant (§7.2 note).
- Product `barcode` — present in the model (Sprint 21 addition) but no explicit uniqueness-check gateway method exists yet; recommend uniqueness per tenant anyway unless barcodes are intentionally shared across variants.
### 9.5 Media validation
File size/type constraints and malware scanning before publish are **recommended, not yet specified precisely** — no size/type limit constants exist in the current frontend code (`docs/architecture/backend/Backend-Platform-API-Spec.md`'s draft §10 also just says "File size/type constraints" without numbers). Pick sane limits (e.g. 10MB images, common web-safe MIME types) and document them in the response envelope so the frontend can render a real client-side pre-check.
### 9.6 Language validation
Every translatable field must expose the full set of `localization.supportedLocales` — a locale missing from `translations` on a required-translation object (nav labels, static-page content per §9.1's `missing-translations` check) should be a validation error, not silently defaulting to the base language, so authors get an explicit signal.
---
## 10. Media
Covered in full in §6.18 (endpoints) and §7.11 (DTO). Summary of concerns not already stated:
- **Upload** is multipart `POST /media/upload`; response is a full `MediaAsset` including server-computed `width`/`height` for images and a `thumbnailUrl`.
- **Delete** is a hard delete (`DELETE /media/:id` → 204) — no soft-delete/restore concept exists for media today; if that's needed, it's a net-new decision, not something to infer from the current contract.
- **Replace** — no dedicated "replace" endpoint; the frontend pattern is delete + re-upload, or `PATCH` for metadata-only changes (alt text, tags, folder, caption, description, decorative flag) without touching the binary.
- **Metadata**: `altText` is a `Record<locale, string>` (translatable, ADR-0001 rule) — never a flat string. `decorative: boolean` intentionally suppresses missing-alt-text warnings for images that are genuinely decorative.
- **Compression/formats/limits/CDN/thumbnail generation** — none of this is specified in the frontend contract today; these are backend implementation choices as long as the `MediaAsset` response shape is honored (`url`, `thumbnailUrl?`, `mimeType`, `size`, `width?`, `height?`).
- **Future storage**: `MockMediaRepository` uses IndexedDB client-side purely as an interim store — this has zero bearing on backend storage choice (S3-compatible object storage, local disk, CDN-fronted, etc. are all compatible with the `MediaAsset.url` contract).
---
## 11. Errors
### 11.1 Standard error envelope
No single error shape is enforced by any HTTP client code today (the frontend mostly just surfaces `error.message` generically) — this is a **recommended standard**, consistent with `docs/architecture/backend/Backend-Platform-API-Spec.md`'s draft, extended to match the fuller field set the spec brief calls for:
```json
{
"code": "validation_error",
"title": "Validation failed",
"message": "One or more fields are invalid.",
"severity": "error",
"details": [
{ "field": "sku", "message": "SKU already exists for this tenant" }
],
"fieldErrors": {
"sku": ["SKU already exists for this tenant"]
},
"retryAfter": null,
"traceId": "trc-8f3a2c1e"
}
```
-`code` — stable machine-readable identifier (snake_case), for programmatic handling.
-`title` — short human summary.
-`message` — fuller human-readable explanation, safe to show a user.
-`severity` — `error | warning | info` (mirrors `ProjectValidationIssue.severity`, §9.1, for consistency between builder-validation issues and generic API errors).
-`details` — array form for multi-field validation failures (matches the draft spec's shape).
-`fieldErrors` — map form of the same information, keyed by field path, for forms that want O(1) per-field lookup instead of scanning `details`.
-`retryAfter` — seconds, populated on 429/503 responses; `null` otherwise.
-`traceId` — correlates a client-visible error to backend logs.
### 11.2 Common codes
`tenant_not_found`, `tenant_suspended`, `unauthorized`, `forbidden`, `validation_error`, `conflict`, `not_found`, `internal_error`, plus the reliability-mode codes below.
### 11.3 HTTP status meanings (repeated from §5.3 with error-body expectations)
| 500 | Unhandled error | error envelope, `code: internal_error` — never leak stack traces or internals in `message` |
| 503 | Service degraded/down | error envelope, `code` from the maintenance-mode set below, `retryAfter` if known |
### 11.4 Degraded-service modes
Not implemented today but recommended so the frontend can render an honest state instead of a generic failure:
-`maintenance_mode` — planned downtime.
-`construction_mode` — tenant not yet fully configured/launched.
-`read_only_mode` — writes disabled, reads still served.
-`feature_disabled` — a specific feature flag is off; distinct from a generic 403.
-`backend_unavailable` / `database_unavailable` / `queue_unavailable` — dependency-specific outage codes so ops/on-call can triage faster than a generic 500.
- **Graceful degradation:** where possible, prefer serving stale/cached data with a warning over a hard failure (e.g. bootstrap should be aggressively cached client + edge side specifically so a backend blip doesn't blank the entire storefront — see §13).
### 11.5 The one known reliability issue in production today
Intermittent `502`/`504 Bad Gateway` on page refresh and browser back-navigation in production. **Root cause is not in this repository.**`environment.production.ts` points the frontend at the backend API via **absolute URLs directly** (`apiUrl: 'https://api.dexarmarket.ru:445'`, `authApiUrl: 'https://users.vitanova.network:456'`), bypassing this repo's own `nginx.conf` entirely (that config only proxies `/api` for one specific tenant host, not `dexarmarket.ru`). The 502/504 originates from **that backend API's own reverse proxy** (ports 445/456, servers not part of this repository). Refresh and back-navigation both re-fire session-check and bootstrap-load calls on mount (`AdminAuthService.checkSession()`, `ConfigService.loadBootstrap()`, `TelegramSessionApiService`), which is the likely trigger if that backend's app server or reverse proxy is crashing, overloaded, or misconfigured on those specific endpoints. **This needs DevOps/backend investigation of upstream health, timeout settings, and concurrent-connection handling around session-check and bootstrap endpoints** — nothing in this frontend repository can fix it.
- **LocaleMap convention** (§5.11): every translatable field is `Record<locale, string>` (or a `translations[locale]` object for richer content) — never a flat hardcoded field. This applies to navigation labels, static-page title/html/seo, product/category translations, footer copyright text.
- **Fallback:** the client resolves a missing translation by falling back toward `defaultLocale` in most rendering paths (e.g. category/item `name` falls back to the first available `names[]` entry, preferring `ru` — see `ApiService.normalizeCategory`/`normalizeItem`, §6.1) — but this is a display-layer fallback for legacy/partial data, not a substitute for actually authoring translations. §9.1's `missing-translations` validation exists specifically to push authors toward complete translations rather than relying on fallback.
- **Default language:** `localization.defaultLocale` must always be a member of `localization.supportedLocales` — enforced client-side (`default-locale-not-supported`, §9.1) and must be enforced server-side too.
- **`Accept-Language` vs. `X-Language`:** the frontend does **not** send `Accept-Language`; it sends its own resolved `X-Language` header (`RU`/`EN`/`AM`, mapped from the user's in-app language selection, §1 step 3) on every API request. Backend should key localized responses off `X-Language`, not the browser's `Accept-Language`.
- **Changing language:** a pure frontend state change (`LanguageService.currentLanguage`) — no backend call is made purely to switch language; the next API request simply carries the new `X-Language` header.
- **Static pages:** localized via `translations[locale].{title,html,seo}` (§4.6) — SEO fields can also be locale-specific (`StaticPageTranslationConfig.seo`), overriding the page-level `seo` block per locale.
- **SEO:** `SeoConfig.default` + `SeoConfig.byPageKey` (§4.4) at the bootstrap level; per-static-page SEO nested under each page's `seo`/`translations[locale].seo` (§4.6). No separate SEO-only endpoint exists — SEO data travels inside bootstrap and static-page payloads.
---
## 13. Caching
| Layer | What | Mechanism today | Recommendation |
|---|---|---|---|
| Bootstrap | `GET /bootstrap` | `ConfigService` caches the whole response in-memory for the app session via `shareReplay(1)`; no HTTP-level caching | Add `ETag`/`Cache-Control: private, max-age=60` (or similar) — safe additive change, client doesn't send conditional headers today so this is a pure improvement, not a breaking change |
| Products/categories (storefront) | `GET /category`, `GET /category/:id`, `GET /items/:id` | `cacheInterceptor` (`src/app/interceptors/cache.interceptor.ts`) — in-memory `Map`, TTL from `CACHE_DURATION_MS` (category list) / `CATEGORY_CACHE_DURATION_MS` (category items + single item), keyed by exact request URL, cleaned up lazily on each access | Backend should still set its own `Cache-Control`/`ETag` for CDN/edge caching — the frontend's cache is per-tab, in-memory, and does not survive a reload |
| Images | media assets | none specified client-side | CDN-fronted with long `max-age` + content-hashed URLs recommended (not currently required by any frontend code) |
| Analytics | dashboard/analytics reads | none — always live | fine to cache short-TTL (seconds) server-side given these are aggregation-heavy |
| Browser cache | general | standard browser HTTP cache applies to anything the backend marks cacheable | — |
| Invalidation | — | today: none — the in-memory `cacheInterceptor` map is purely TTL-based, no explicit invalidation hook exists anywhere (e.g. a product update does not proactively bust the `GET /items/:id` cache entry) | if strong consistency after an admin write matters, either shorten `CATEGORY_CACHE_DURATION_MS` further or add an explicit cache-bust mechanism — not present today |
**Bootstrap caching is the most consequential** — since it's loaded once and held for the entire session, any admin publish (§6.7) will not be visible to already-open customer tabs until they reload. Consider a low-TTL `Cache-Control` on `GET /bootstrap` plus (future) a lightweight "config changed" push/poll signal if near-real-time propagation matters — no such mechanism exists in the frontend today.
---
## 14. Backend replacement pattern
This is the mechanical pattern **every** domain in this document follows, and the reason implementing a backend for any one domain is low-risk and additive:
Token -.rebind to.-> Api["*ApiGateway (real HTTP, implements same interface)"]
Api --> Backend["Backend REST API"]
```
1. A `*Gateway` TypeScript interface defines the domain's contract (e.g. `AdminProductsGateway`, `AdminCategoriesGateway`, `MediaRepository`).
2. A `*LocalGateway` (or, for media, `MockMediaRepository`) implements that interface today, bound via an Angular DI token/injection.
3. To go live: implement a `*ApiGateway` class against the **same interface** (same method signatures, same DTOs — §7), hitting the real endpoints documented in §6.
4. Rebind the DI token to the new class in the app's provider config (`app.config.ts` or the relevant feature's providers).
5.**Facades and page components do not change.** This is the whole point of the pattern (ADR-004's "configuration provider abstraction" generalizes to every domain, not just bootstrap).
Provider *selection* (mock vs. api) for bootstrap/backoffice/products/categories specifically goes through `RuntimeProviderStrategyService` (`src/app/core/providers/runtime-provider-strategy.service.ts`):
```ts
type RuntimeProviderMode = 'mock' | 'api' | 'remote-config';
class RuntimeProviderStrategyService {
getBootstrapProviderMode(): RuntimeProviderMode {
// 'mock' if environment.useMockData, OR (useMockBootstrapOnLocal && running on localhost)
getProductProviderMode(): RuntimeProviderMode { /* same */ }
getCategoryProviderMode(): RuntimeProviderMode { /* same */ }
}
```
Note the asymmetry: bootstrap can be mocked on localhost independent of `useMockData` (`useMockBootstrapOnLocal: true` by default) so local dev never needs a live bootstrap endpoint, while product/category/backoffice mocking is a single global `useMockData` switch. `'remote-config'` is a declared-but-unused third mode — reserved, not implemented anywhere today.
---
## 15. Future APIs
Reserved sections only — **do not implement speculative endpoints for these**; document intent, not a contract, until real requirements exist. No frontend code references any of the following today.
| Domain | Notes |
|---|---|
| Coupons | `featureFlags.coupons` boolean already exists in bootstrap (unused beyond the flag itself) |
| Discounts | `AdminProduct.discount: number` exists (§7.2) as a static per-product value; a real discount/promotion *engine* (rules, date ranges, stacking) does not exist |
| Marketplace Partners | not modeled — this platform is single-tenant-per-marketplace, not itself a marketplace-of-marketplaces |
| Inventory | `AdminProduct.quantity`/`stockStatus` exist per-product; no warehouse/location-level inventory model |
| Warehouses | not modeled |
| Delivery | `DeliveryOption` exists for storefront cart display (`src/app/models`, referenced by `ApiService`) — a full delivery/logistics domain (carriers, rates, tracking beyond `AdminOrderShipping.trackingNumber`) does not exist |
| Invoices | `featureFlags.invoices` boolean exists; `AdminOrdersGateway` UI has a "print invoice" action today but it's client-rendered from existing order data, not a backend invoice document |
| Refunds | `requestRefund()` exists (§6.11, §8.2) as a status transition; a full refund-processing domain (partial refunds, refund reasons, ledger) does not |
| Webhooks | `AdminWebhookDelivery` model exists for *displaying* delivery status (§7.9) but no webhook *registration/configuration* surface exists anywhere |
| Audit Logs | `AdminMonitoringEvent`/`AdminUserAuditEntry`/order `timeline[]`/review `timeline[]`/transaction `audit[]` are all narrow, per-domain audit trails today; a unified cross-domain audit log does not exist |
| Roles | `AdminRole`/`PermissionsConfig` exist as data shapes (§7.5–§7.6) but are not enforced anywhere server-side yet (§2.5, §3.3) |
| Permissions | same as Roles — modeled, not enforced |
| Feature Flags (management API) | `FeatureFlagsConfig`/`MarketplaceFeaturesConfig` are read via bootstrap only; no dedicated `GET/PUT /feature-flags` endpoint is called by any frontend code today (the draft spec in the old `Backend-Platform-API-Spec.md` proposed one — reasonable to build, just not yet consumed) |
---
## 16. Sequence diagrams
### 16.1 Authentication (customer or admin — identical backend flow, different client storage)
```mermaid
sequenceDiagram
participant U as User (browser)
participant FE as Frontend (AuthService / AdminAuthService)
participant TG as Telegram Bot
participant BE as Backend (users/sessions)
FE->>BE: POST /users/sessions {webSessionID}
BE-->>FE: {webSessionID, ...}
FE->>U: Render QR / deep link (t.me/{bot}?start={webSessionID})
U->>TG: Scan QR / tap deep link
TG->>BE: (out of band) associate Telegram identity with webSessionID
loop every 5s, up to 100x
FE->>BE: GET /users/sessions/:webSessionID
BE-->>FE: {active: false, ...}
end
TG->>BE: user completes bot interaction
FE->>BE: GET /users/sessions/:webSessionID
BE-->>FE: {active: true, user: {...}, expiresAt}
FE->>FE: activateSession() -> set cookie, start refresh timer
```
### 16.2 Bootstrap load
```mermaid
sequenceDiagram
participant App as Angular App
participant CS as ConfigService
participant Prov as ApiBootstrapProvider / MockBootstrapProvider
participant BE as Backend
App->>CS: loadBootstrap()
alt already cached this session
CS-->>App: cached BootstrapConfig (shareReplay)
else not cached
CS->>Prov: loadBootstrap()
alt mock mode / localhost
Prov-->>CS: bootstrap.json (local asset)
else real
Prov->>BE: GET /bootstrap (Host header resolves tenant)
### 16.9 Order creation — FUTURE (no order-creation endpoint exists anywhere yet; orders today are admin-managed post-hoc, not created via the documented API)
```mermaid
sequenceDiagram
participant Shopper
participant FE as Frontend (checkout, not yet built against a real order endpoint)
participant BE as Backend
Note over Shopper,BE: No frontend code creates an AdminOrder today. Checkout today only calls the cart-payment endpoints (§16.5). Order creation as a distinct backend concept (order number, items snapshot, timeline) is a FUTURE integration point once checkout and the admin order domain are connected.
Shopper->>FE: complete checkout
FE->>BE: (future) POST /orders {items, customer, payment, shipping}
BE-->>FE: (future) 201 AdminOrder
```
---
## 17. Developer notes
### Performance
-`GET /category`/`GET /category/:id`/`GET /items/:id` are hit frequently and already client-cached (§13) — backend should still index these for fast reads (category id, item id are the obvious indexes) since the client cache is per-tab and short-lived.
- Paginated admin list endpoints (§5.4) must support `page`/`pageSize` server-side, not return-everything-then-paginate-client-side — several mock gateways currently return full arrays and let the facade slice, which will not scale once real data volumes exist.
### Scaling
- Tenant resolution happens on every request (host header lookup) — this must be a fast, cached lookup (e.g. an in-memory or Redis-backed host→tenant map), not a database round-trip per request, given it gates literally everything including the public bootstrap endpoint.
-`BackofficeDataService` (frontend) already shares one in-flight request per endpoint across multiple consumers via `shareReplay` — backend need not defend against duplicate near-simultaneous requests from a single client session, but should still handle normal concurrent-tenant load.
- Session: `webSessionID`/`adminSessionID` (primary lookup key on every authenticated request).
### Transactions & concurrency
-`PUT /builder/bootstrap/draft` and `POST /builder/bootstrap/publish` are whole-document writes — guard against two concurrent editors clobbering each other. No optimistic-locking field (`version`/`etag`) exists in `BootstrapConfig` today; adding one (e.g. `schemaVersion` bumped or a separate `revision` field checked on write) is recommended before multi-editor concurrent use is expected, since the current contract has no way to detect a stale-draft overwrite.
- Order status transitions (§8.1) should be guarded by a DB-level transaction covering the status write + timeline-entry append, so the two never diverge.
- Category `deletedAt`/product `archived` toggles plus any cascading effects (e.g. hiding a category's products) should be transactional if such cascades are implemented.
### Versioning
-`schemaVersion` in `BootstrapConfig` is the real version signal (§5.1) — bump it deliberately, coordinate with frontend releases for anything beyond additive optional fields.
- No API-path versioning exists (`/bootstrap`, not `/v1/bootstrap`) — introducing path versioning later is a breaking change to every existing integration; if it's ever needed, prefer a new parallel path family over renaming the existing one.