docs: finalize BACKEND.md as a self-contained implementation prompt
Some checks failed
Architecture Governance / architecture (push) Has been cancelled

Goal: this document alone, pasted into a fresh Claude session, should
be enough for a backend dev to implement against without getting
stuck or inventing conventions the frontend doesn't actually need.

Fixed a real bug: ~65 cross-references throughout the document pointed
to docs/AUTHENTICATION.md, docs/ERROR_CONTRACT.md, docs/MAINTENANCE_MODE.md
- three sibling docs that were deleted and fully merged into this
document's own §4/§6/§10 during the earlier doc-consolidation pass, but
the in-text references were never updated. A fresh agent following
those links would hit dead ends repeatedly. Bulk-replaced with in-
document section references; hand-fixed ~4 sentences that framed §6/
§10 as "sibling task, in progress" (stale - both are complete, this
document's §6/§10 already are the settled contract, nothing to wait on).

Added "Recommended default" to every item across the document's 5
consolidated "Requires backend decision" registers (§2.12 framework,
§3.21 CRUD cross-cutting - 18 items, §4 §12 auth open items - 9 items,
§6 error-model summary - 9 items, §10 maintenance-mode list - 5 items).
Each default is derived from what the frontend already implies or
standard REST/security convention - no invented APIs or business
rules. ~6 items are explicitly flagged as real business/security
decisions instead (order state-machine rules, Ed25519 cutover
strategy, refresh-token reuse-detection posture) since those carry
consequences no amount of frontend-code-reading can resolve.

Added a "How to use this document" preamble up front: work in §9's
dependency order not document order, apply recommended defaults and
keep moving, only stop for the explicitly-flagged business/security
items, don't invent beyond what's written or directly implied.

Frontend side: confirmed nothing else is left. docs/TODO.md already
has zero blockers; docs/KNOWN-ISSUES.md's one open item (unreachable
Ed25519 error-code UI) is correctly left open and documented rather
than faked closed - fixing it needs a real backend emitting real
distinguishable error codes, which doesn't exist yet and can't be
fabricated without inventing an API contract.

No frontend code touched. No architecture changed. No APIs invented.
This commit is contained in:
sdarbinyan
2026-07-26 16:10:33 +04:00
parent a32c3f241d
commit eba9b7f4f0

View File

@@ -1,11 +1,52 @@
# Backend — Canonical Specification
**This is the ONE document a backend engineer needs.** It is the single source of truth for backend implementation — architecture, bootstrap, authentication, JWT, Ed25519 public-key login, permissions, maintenance mode, error contract, every endpoint, DTOs, request/response schemas, uploads, pagination, filters, sorting, publish workflow, media, builder, examples, and a top-to-bottom implementation checklist. It supersedes and fully merges `AUTHENTICATION.md`, `ERROR_CONTRACT.md`, `MAINTENANCE_MODE.md`, and (already archived, content re-derived from current source) `docs/archive/BACKEND_API.md` and `docs/archive/BACKEND_API_REMAINING_WORK.md`. Those standalone files no longer exist — everything they contained lives here.
**This is the ONE document a backend engineer needs.** It is the single source of truth for backend implementation — architecture, bootstrap, authentication, JWT, Ed25519 public-key login, permissions, maintenance mode, error contract, every endpoint, DTOs, request/response schemas, uploads, pagination, filters, sorting, publish workflow, media, builder, examples, and a top-to-bottom implementation checklist. Authentication (§4), the Error Model (§6), and Maintenance Mode (§10) are sections of this same document, not separate files — earlier drafts existed as standalone `AUTHENTICATION.md`/`ERROR_CONTRACT.md`/`MAINTENANCE_MODE.md` files and were fully merged in; those files no longer exist. This document also supersedes (already archived, content re-derived from current source) `docs/archive/BACKEND_API.md` and `docs/archive/BACKEND_API_REMAINING_WORK.md`.
Everything here is derived from the actual current frontend source code (branch `B2B`), not from prior/stale documentation. Primary input: `docs/context/BACKEND-AUDIT.md` (exhaustive audit of every HTTP call, gateway, facade, and model in the frontend).
**Convention used throughout:** where the frontend already implies a concrete behavior, it's documented as-is. Where the frontend has no opinion and a real backend needs one, it's marked **"Requires backend decision"** — nothing beyond what the frontend requires is invented.
## How to use this document (read this first if you're implementing from it)
You are building a real backend for an existing, feature-complete Angular
frontend (branch `B2B`) that currently runs entirely on mock/local data. This
document is the complete contract — every endpoint, DTO, status code, and
convention the frontend already expects. Treat it as your spec, not as
background reading.
1. **Work in dependency order**, not top-to-bottom document order: §9 (Backend
Checklist) gives the actual build sequence in 6 phases (foundation → auth/
tenant/bootstrap → read-heavy catalog → write-heavy customer domains →
admin domains → builder/CMS → hardening). Start there.
2. **Every "Requires backend decision" marker has a Recommended default**
attached to it (in §2.12, §3.21, §4 §12, §6's summary, and §10's list —
the five per-section consolidated registers). Apply the recommended
default and keep moving. Do not stop and wait for human input on these —
they exist so you don't have to invent a convention from nothing, and
they were chosen to match what the frontend already assumes.
3. **A small number of items are explicitly marked as business/security
decisions, not technical ones** (e.g. order status-transition rules, the
Ed25519 cutover strategy, refresh-token reuse detection posture). These
are the only points where you should actually stop and ask a human,
because they carry money, legal, or security consequences the frontend
code can't tell you the answer to.
4. **Do not invent new endpoints, fields, or business rules beyond what's
written here or directly implied by an existing frontend call.** If you
find yourself needing something this document doesn't cover and no
recommended default applies, that's a real gap — flag it explicitly
rather than guessing silently.
5. **§8 (Real Backend Implementation Guide)** has the frontend-side
mechanics: which DI-token gateways to swap, which facades change, worked
DTO-mapping examples. Read it before touching any single domain's
endpoints in §3 — it explains the pattern every domain repeats.
6. **Verify against the live frontend where you can.** Every literal
(non-PROPOSED) path, DTO, and example in this document was checked
against actual source on `B2B` — but the frontend evolves. If your
implementation and this document ever disagree with what the running
frontend actually sends/expects, the frontend's actual behavior wins;
flag the doc as stale rather than building against a description that
no longer matches reality.
## Table of contents
1. [Bootstrap](#1-bootstrap)
@@ -172,7 +213,7 @@ Each nested field, with its source model file under
- **`permissions`** (`PermissionsConfig`, `permissions.model.ts`) —
`{ definitions: [{ key, description? }], roles: [{ role, permissions[] }] }`.
Bootstrap-level RBAC catalog. (Distinct from the Ed25519 JWT `AdminRole`
union — see `docs/AUTHENTICATION.md` §9 for the flagged `AdminRole` naming
union — see `§4 (Authentication, this document)` §9 for the flagged `AdminRole` naming
collision.)
- **`header`** (`HeaderConfig`, optional) — header layout config.
- **`catalog`** (`CatalogConfig`, `catalog-config.model.ts`, optional) — the
@@ -392,7 +433,7 @@ trimmed for length; full versions in that file):
- There is **no `APP_INITIALIZER`** wiring bootstrap fetch as a hard app
precondition. `main.ts` calls `bootstrapApplication(App, appConfig)` with no
initializer that blocks on config. (`docs/AUTHENTICATION.md` §5.2 separately
initializer that blocks on config. (`§4 (Authentication, this document)` §5.2 separately
notes the Ed25519 `restoreSession()` initializer is also not wired.)
- Bootstrap is instead loaded lazily-but-eagerly by the first consumer to need
it. Multiple root-level services subscribe to `ConfigService.loadBootstrap()`
@@ -441,7 +482,7 @@ strategy and the frontend handling for it would be new work.
### 1.9 Tenant resolution
Tenant is resolved by **subdomain**, not by header or path (full detail in
`docs/AUTHENTICATION.md` §10; source
`§4 (Authentication, this document)` §10; source
`src/app/core/config/tenant-resolver.service.ts`):
- `TenantResolverService.getTenantKey()`: localhost →
@@ -498,7 +539,7 @@ there is no separate "draft bootstrap" vs "live bootstrap" endpoint today
error.
- Feature-specific consumers that require bootstrap data render their own
generic empty/error states (the same `common.errorTitle` pattern described in
`docs/ERROR_CONTRACT.md`), not a bootstrap-specific one.
`§6 (Error Model, this document)`), not a bootstrap-specific one.
> **Requires backend decision / frontend follow-up:** whether a hard bootstrap
> failure should present a dedicated tenant-level error screen. Today the
@@ -513,13 +554,13 @@ The frontend does not parse a bootstrap-specific error body — a failed
the standard error envelope every endpoint (including this one) should return
on non-2xx, and the per-status semantics (401/403/404/409/422/429/500/503,
plus `TENANT_DISABLED` and `MAINTENANCE_MODE`), see
**`docs/ERROR_CONTRACT.md`** — that document owns the error envelope and this
**§6 (Error Model)** — that section owns the error envelope and this
section does not redefine it. Two of its rows are especially relevant to
bootstrap:
- **`TENANT_DISABLED`** (HTTP 403, `error.code: "TENANT_DISABLED"`) — the
natural failure for `GET /bootstrap` against a known-but-inactive tenant.
`ERROR_CONTRACT.md` marks the frontend handling for this as "Requires backend
`§6` marks the frontend handling for this as "Requires backend
decision" (no code path handles it today).
- **`MAINTENANCE_MODE`** (HTTP 503, `error.code: "MAINTENANCE_MODE"`) — if a
tenant is down for maintenance, bootstrap is where the frontend would first
@@ -681,7 +722,7 @@ defaults defensively:
### 2.7 JWT / authentication header format
Two coexisting mechanisms; full spec in **`docs/AUTHENTICATION.md`** (this
Two coexisting mechanisms; full spec in **`§4 (Authentication, this document)`** (this
section only references it, does not redefine):
- **Telegram session auth (LIVE)** — no `Authorization: Bearer`. Identity is a
@@ -693,12 +734,12 @@ section only references it, does not redefine):
- **Ed25519 admin auth (wired, backend absent)** — issues a JWT
`AuthTokenPair { token, refreshToken }`; the intended header is standard
`Authorization: Bearer <token>`, but its `authInterceptor` is **not
registered** today (AUTHENTICATION.md §2.6), so no request auto-attaches the
registered** today (§4 §2.6), so no request auto-attaches the
bearer token yet. `adminAuthHeadersInterceptor` *does* set `Authorization:
Bearer <token>` if an admin token happens to be stored, but nothing stores one
in the live flow.
Refer to `docs/AUTHENTICATION.md` for token structure (`JwtClaims`), refresh,
Refer to `§4 (Authentication, this document)` for token structure (`JwtClaims`), refresh,
rotation, expiry, role hierarchy, and tenant-scoping open items.
### 2.8 Live endpoint domains (real HTTP, already implemented)
@@ -706,13 +747,13 @@ rotation, expiry, role hierarchy, and tenant-scoping open items.
The following domains have **real `HttpClient` calls in code today** (not
proposals). Documented per-endpoint below. (The Ed25519 admin-auth API is wired
to real HTTP but the backend does not implement it yet — its full contract is
in `docs/AUTHENTICATION.md` §2, not repeated here.)
in `§4 (Authentication, this document)` §2, not repeated here.)
#### 2.8.1 Session auth API (LIVE)
Base: `environment.authApiUrl` (= `https://api.dexarmarket.ru:445`). Source:
`src/app/services/telegram-session-api.service.ts`. Narrative flow in
`docs/AUTHENTICATION.md` §1.
`§4 (Authentication, this document)` §1.
| Endpoint | Method | Auth | Body / Headers | Response |
|---|---|---|---|---|
@@ -771,7 +812,7 @@ GET https://api.dexarmarket.ru:445/users/sessions/3f1c2a0e-4e21-4d3a-9e77-1e8f6a
```
The frontend reads fields **field-tolerantly** (accepts many casings/aliases —
see `normalizeWebSession`, and `docs/AUTHENTICATION.md` §1.3 for the full alias
see `normalizeWebSession`, and `§4 (Authentication, this document)` §1.3 for the full alias
priority lists). A backend should send a real `expiresAt`/`expires` (else the
frontend fabricates `now + 3600s`).
@@ -1047,10 +1088,10 @@ against `docs/context/BACKEND-AUDIT.md` (the this-session audit — the ground
truth for what code actually does), the prior `docs/archive/BACKEND_API.md`, and the two
sibling specs written this session:
- **Auth / headers / JWT** — see `docs/AUTHENTICATION.md`. This section never
- **Auth / headers / JWT** — see `§4 (Authentication, this document)`. This section never
re-defines the auth header format; it references it.
- **Error response envelope + per-status semantics** — see
`docs/ERROR_CONTRACT.md`. This section names *which* statuses apply per
`§6 (Error Model, this document)`. This section names *which* statuses apply per
endpoint and *why*, but the wire shape of the error body is owned by that doc.
### 3.0 Conventions used in this section
@@ -1061,12 +1102,12 @@ sibling specs written this session:
(`src/app/core/config/api-config.service.ts`). Localhost → `/api`; otherwise a
per-tenant origin (default `https://api.dexarmarket.ru:445`). Tenant isolation
is **by subdomain/base-URL only** — no `X-Tenant` header, no `/tenant/{id}`
prefix (`AUTHENTICATION.md` §10). All storefront reads and all admin CRUD go
prefix (`§4` §10). All storefront reads and all admin CRUD go
here.
- **Payment / QR API** — `environment.qrApiUrl` (`https://qr.vitanova.network/api`).
Only cart/order payment + status polling (§3.3).
- **Session-auth API** — `environment.authApiUrl`. Login/session only, covered by
`docs/AUTHENTICATION.md`, not repeated here.
`§4 (Authentication, this document)`, not repeated here.
**LIVE vs PROPOSED paths.** Only the storefront catalog/cart/engagement reads and
the single `AdminCategoriesApiGateway` have **literal HTTP paths in code**. Every
@@ -1102,34 +1143,34 @@ FUTURE** throughout.
`/admin/`, `/backoffice/`, `/builder/`, or `/media/` **additionally** get
`AdminWebSessionID` and, if an admin token is stored, `Authorization: Bearer
<token>` (`adminAuthHeadersInterceptor`). These header names/values are owned by
`AUTHENTICATION.md` — assume them on every admin endpoint below unless stated.
`§4` — assume them on every admin endpoint below unless stated.
**JWT requirement.** No endpoint in the app requires a verified JWT **today**.
Mechanism A (Telegram session) authenticates admins with an opaque
`AdminWebSessionID`, not a JWT; the Ed25519 JWT flow (Mechanism B) is fully wired
but dormant (`AUTHENTICATION.md` §2). For every admin endpoint below, "JWT
but dormant (`§4` §2). For every admin endpoint below, "JWT
requirement" is therefore stated as: **Today: `AdminWebSessionID` session header
(Mechanism A). Target: `Authorization: Bearer <Ed25519 JWT>` once Mechanism B is
cut over.** Server-side authorization must be enforced regardless of any
client-side guard (`AUTHENTICATION.md` §11.6).
client-side guard (`§4` §11.6).
**Required permission / role.** The frontend's only permission model is the
coarse `ROLE_PERMISSIONS` table (`AUTHENTICATION.md` §9.1): `backoffice.read`,
coarse `ROLE_PERMISSIONS` table (`§4` §9.1): `backoffice.read`,
`backoffice.write`, `builder.read`, `builder.write`, `users.manage`,
`settings.manage`. The frontend does **not** enforce per-domain permissions on
admin CRUD today (the live `adminAuthGuard` only checks "is there an admin
session at all"). Where a specific permission is the obvious fit it is named as
**Proposed**; any finer per-role assignment is **Requires backend decision**
(`AUTHENTICATION.md` §9.1 already flags fine-grained permissions as out of scope).
(`§4` §9.1 already flags fine-grained permissions as out of scope).
**Error responses.** Unless a domain-specific note says otherwise, every endpoint
below can return the envelope from `ERROR_CONTRACT.md` with these statuses:
below can return the envelope from `§6` with these statuses:
`401 UNAUTHENTICATED` (no/expired session), `403 FORBIDDEN` (wrong role/tenant),
`500 INTERNAL_ERROR`, `503 SERVICE_UNAVAILABLE`/`MAINTENANCE_MODE`. Per-endpoint
notes below add `404`, `409`, `422`, `429` only where they are meaningful. **The
frontend does not parse the error body today** (`ERROR_CONTRACT.md` "Finding") —
frontend does not parse the error body today** (`§6` "Finding") —
admin list pages collapse every non-2xx into one generic "retry" state
(`ERROR_CONTRACT.md` "Generic list-page error UI"). So all per-status handling
(`§6` "Generic list-page error UI"). So all per-status handling
below is the *contract the backend should honor*, not behavior the current UI
distinguishes.
@@ -1186,7 +1227,7 @@ should treat it as authoritative for accepted field name variants; see
is the only one returning a `total` for page math.
- **Error responses:** `404` when `itemID` doesn't exist (P1) — but note the
frontend does **not** distinguish 404 from any other error today
(`ERROR_CONTRACT.md` §404); a deleted product and a 500 render the same generic
(`§6` §404); a deleted product and a 500 render the same generic
empty-state. `500/503` generic. Calls retry x2 with exponential backoff
(`ApiService.retryConfig`).
- **Validation:** none (read).
@@ -1325,10 +1366,10 @@ DTO: `AdminCategory` (`src/app/features/admin/categories/models/admin-category.m
- **AC7:** proactive slug-uniqueness pre-check the frontend calls *before*
submit (`excludingId` omitted on create, set to the editing id on update). This
is the only proactive conflict check in the whole admin surface. On network
error it returns `false` (best-effort, TOCTOU-prone — `ERROR_CONTRACT.md` §409).
error it returns `false` (best-effort, TOCTOU-prone — `§6` §409).
- **Validation:** slug uniqueness via AC7; everything else none client-side.
`title`, `slug` are non-nullable in the model. Recommended `409 CONFLICT` on
AC3/AC4 slug collision as a race backstop (`ERROR_CONTRACT.md` §409), and
AC3/AC4 slug collision as a race backstop (`§6` §409), and
`422` for field errors.
- **Error responses:** AC2/AC6 swallow errors → `null` (so a `404` there renders
as "not found"/empty, not an error screen). AC5 hard `404` if unknown. `409`
@@ -1384,7 +1425,7 @@ these live calls (`BACKEND-AUDIT.md` §10). Payment DTOs are inline in
any of the documented aliases.
- **Error responses:** O5 is fire-and-forget — a failure must **never** block the
payment-confirmed flow (source comment, line 635). `401` on a customer-facing
`/cart`/`/orders` has **no unified re-auth UX** today (`ERROR_CONTRACT.md` §401,
`/cart`/`/orders` has **no unified re-auth UX** today (`§6` §401,
Requires backend decision). Payment status polling handles its own timeouts.
- **Validation:** none client-side. Amounts/quantities ≥ 0, valid email/phone =
Requires backend decision.
@@ -1502,7 +1543,7 @@ Contract: `AdminUsersGateway`
- **Headers/JWT:** admin auth (§3.0).
- **Permission (Proposed):** all user/role/invitation/session management →
`users.manage` (the one permission specifically about this domain — see
`AUTHENTICATION.md` §9.1). Read-only U1-U5 could be `backoffice.read` — Requires
`§4` §9.1). Read-only U1-U5 could be `backoffice.read` — Requires
backend decision.
- **U6 body:** `{ roleId: string }`. **U7 body:** `{ status: AdminUserStatus }`
where status ∈ `active|invited|suspended`. **U8 body:** `{ email, roleId,
@@ -1511,7 +1552,7 @@ Contract: `AdminUsersGateway`
— **a different type from the auth `AdminRole` string-union** used by the JWT
`role` claim (`Owner|Administrator|Editor|Support|ReadOnly`,
`src/app/core/auth/models/permission.model.ts`). **This is the flagged
duplicate** (`BACKEND-AUDIT.md` §14, `AUTHENTICATION.md` §9). Reconciliation
duplicate** (`BACKEND-AUDIT.md` §14, `§4` §9). Reconciliation
needed: treat the auth string-union as the JWT/role-claim contract; treat this
interface as an admin-users-management row. Recommend renaming the latter (e.g.
`AdminUserRoleRecord`). **Requires backend decision / naming reconciliation.**
@@ -1522,7 +1563,7 @@ Contract: `AdminUsersGateway`
- **Sessions/audit:** `revokeSession` (U10) exists as a mock method; whether an
Owner can remotely kill another admin's live session, and how that propagates to
the already-logged-in client, is **Requires backend decision**
(`AUTHENTICATION.md` §8 — no push/poll mechanism exists client-side).
(`§4` §8 — no push/poll mechanism exists client-side).
- **Validation:** none client-side. Email format, role existence, self-demotion
guards = Requires backend decision.
- **Error responses:** `404` (unknown user/session/invitation), `409` (e.g.
@@ -1544,7 +1585,7 @@ reconciliation the backend must make (this is the audit's flagged item):
- **`AdminRole` #1** — string union `'Owner'|'Administrator'|'Editor'|'Support'
|'ReadOnly'` (`src/app/core/auth/models/permission.model.ts`). Used by the JWT
`role` claim and `PermissionService`. Ordered high→low by convention only. Maps
to permissions via `ROLE_PERMISSIONS` (`AUTHENTICATION.md` §9.1). **This is the
to permissions via `ROLE_PERMISSIONS` (`§4` §9.1). **This is the
role contract for authorization.**
- **`AdminRole` #2** — interface `{ id, name, permissions: string[], builtIn }`
(`src/app/features/admin/users/models/admin-user.model.ts`). A management row for
@@ -1646,7 +1687,7 @@ view model: `ContentPage` (`src/app/features/content-management/models/content-p
bootstrap is published).
- **Validation:** the client-side `ContentPageService.validatePages()` +
`ProjectValidator` convention exists (`ProjectValidationIssue { code, message,
section, fieldKey, severity }`) and is the shape `ERROR_CONTRACT.md` §422
section, fieldKey, severity }`) and is the shape `§6` §422
recommends aligning backend `details[]` to. Actual required fields (slug
uniqueness, non-empty title) = Requires backend decision.
- **Error responses:** `404` (CM2/CM4/CM5), `409` (duplicate slug — proposed),
@@ -1891,7 +1932,7 @@ sections already covered (§3.8-3.14) and `BootstrapConfig`'s remaining sub-conf
`productPage`, `userExperience`, `company`, `apiEndpoints`, `permissions`). All of
these are **served inside `GET /bootstrap`** and edited (where editable) via the
project-editor in-memory (LOCAL-ONLY). The only permission that names settings is
`settings.manage` (Owner-only, `AUTHENTICATION.md` §9.1).
`settings.manage` (Owner-only, `§4` §9.1).
The unifying gap: **there is no client-side "publish/save bootstrap" HTTP call
anywhere** (`BACKEND-AUDIT.md` §17, §25.5). Everything in §3.8-3.15 is persisted
@@ -2129,46 +2170,106 @@ token-bound admin gateway. Proposed: `GET {base}/backoffice/dashboard/metrics`
### 3.21 Cross-cutting "Requires backend decision" register (this section)
Consolidated so the master doc can dedupe against the auth/error registers:
Consolidated so the master doc can dedupe against the auth/error registers.
Each item has a **Recommended default** — apply it unless you have a specific
reason not to; only stop and ask a human where none is given or the item has
real business/money/legal weight (marked accordingly).
1. **DI seam prerequisite** — orders, products, users, transactions, monitoring,
moderation (+ derived customers/analytics) inject their `*LocalGateway`
concretely; a backend requires introducing a DI token first (`BACKEND-AUDIT.md`
§14). Only categories + dashboard-metrics are token-bound today.
**Recommended default:** this is frontend work (adding the token seam), do
it for every domain before wiring its real gateway — mechanical, no backend
input needed. See §8.5 for the exact per-domain pattern.
2. **All admin CRUD paths are PROPOSED** — only `/backoffice/categories*` and the
storefront catalog/cart/engagement paths are literal. Adopt the
`/backoffice/<domain>` convention (or reject it).
**Recommended default:** adopt it as-is — it's already live for categories,
changing it later is a bigger cost than any alternative naming would save.
3. **Server-owned field lists** on create/update for products/orders/etc. (only
categories defines one, by stripping `id/itemsCount/deletedAt/createdAt/updatedAt`).
**Recommended default:** mirror the categories pattern exactly (server strips
`id`/counts/timestamps from client payloads; client never sends them).
4. **Field validation + error wording** — the frontend enforces essentially none
on admin CRUD (no reactive `Validators`); all `422` field rules and messages are
backend-owned.
**Recommended default:** validate required/type/length server-side per the DTO
shapes given in §3; return field errors in the §6 `422` shape (`fieldErrors`
keyed by field path). Don't wait on frontend validation to be added first.
5. **Product slug/sku uniqueness** — no proactive check exists (unlike categories'
`isSlugTaken`); decide `409` behavior.
**Recommended default:** mirror categories — add an `isSlugTaken`-equivalent
check endpoint, and return `409` with the §6 conflict shape on collision.
6. **Order status-transition legality** and refund eligibility rules.
**Business decision, not technical** — the valid state machine and who can
trigger which transition depends on the merchant's actual fulfillment
process. Ask.
7. **Customers as a first-class resource** vs. derived-from-orders aggregate.
**Recommended default:** derived-from-orders aggregate (§3.4 as written) —
it's what the frontend already assumes and needs no new storage model. Only
promote to first-class if a real need appears (e.g. customer accounts,
guest-order merging).
8. **`AdminRole` duplication** — auth string-union vs. users-management interface
(naming reconciliation; role CRUD / custom roles).
**Recommended default:** keep the coarse `AdminRole` string union (§4 §9.1)
as the enforced permission source; treat the users-management interface's
richer shape as display-only until custom roles are actually requested by
the product.
9. **Remote session revocation propagation** (`revokeSession`) to a logged-in
client (`AUTHENTICATION.md` §8 — no push/poll exists).
client (`§4` §8 — no push/poll exists).
**Recommended default:** accept the propagation delay (revoked session stays
client-usable until its next natural refresh-interval tick, §4 §8) rather
than building push/poll infrastructure for this alone — revisit only if a
real incident makes the delay unacceptable.
10. **Media**: allowed mime/size; whether in-use assets are delete-blocked.
**Recommended default:** allow the mime types the client already filters
for (`image/*`, `application/pdf`, `image/svg+xml` — §7.1 `MediaAssetKind`),
10MB per file; block delete of in-use assets with a `409` listing the
referencing entities (safer default than silent broken references).
11. **CMS/homepage/widgets/nav/footer/branding/languages/settings are LOCAL-ONLY**
— the single biggest decision is **whole-bootstrap publish (S2) vs. granular
builder endpoints** (§3.15). No builder write call exists in code today.
**Recommended default:** whole-bootstrap publish (a single `PUT`/`POST` that
replaces the tenant's bootstrap document) — matches how the frontend already
edits it as one in-memory draft object; granular per-section endpoints are
strictly more backend work for no frontend benefit today.
12. **Languages**: duplicate-add `409`, removing the default/last locale, BCP-47
validation.
**Recommended default:** `409` on duplicate-add; reject removing the last
remaining locale or the current default (`422`); validate against BCP-47
syntax, not a fixed enum (frontend already treats locale as an open string).
13. **Transactions ↔ live payment records** are unconnected models; how they link.
**Recommended default:** key `AdminTransaction` off the same order/payment id
the live QR/card flow already returns (§3.3.a) rather than inventing a new
identifier — one payment, one transaction record.
14. **Reviews**: storefront `Review` vs. admin `AdminReview` linkage; how a
submitted review enters the moderation queue.
**Recommended default:** every storefront-submitted review starts in the
moderation queue at `pending` status; `AdminReview` is the same underlying
record with moderation fields attached, not a separate entity.
15. **Reports**: no creation flow exists — who creates reports.
**Business decision** — depends on whether "reports" means customer-flagged
content, automated abuse detection, or admin-initiated audits. Ask.
16. **Monitoring**: event windowing/pagination for a real stream; webhook
*management* (vs. read-only delivery log) is out of scope.
**Recommended default:** offset-paginated `GET` with a `since`/time-range
filter (matches §2.2's standard pagination contract) — no need for a
streaming protocol for an admin-facing log view.
17. **Analytics**: `conversionRate`/`retentionPercent` must stay `null` when
unknown; no traffic/visitor data source exists anywhere — real analytics is a
from-scratch backend build.
**Recommended default:** ship order/product/revenue aggregates first (real
data, computable from orders/products already in scope); leave traffic-based
metrics `null` until a real analytics/traffic pipeline exists — never
fabricate a number, the frontend already renders `null` as "Unknown."
18. **Variants/options** shape reconciliation between storefront `variantOptions`
(grouped) and admin `variants` (flat priced rows); no dedicated variants
endpoint (created within the product body).
**Recommended default:** keep variants embedded in the product body (no
separate variants endpoint) — matches how the admin editor already sends
them; the backend computes/derives the storefront's grouped
`variantOptions` shape from the same flat rows at read time.
---
@@ -2948,44 +3049,73 @@ of anything to the backend.
### 12. Open items — "Requires backend decision"
Consolidated list of everything this document could not derive from
existing frontend code and therefore does not prescribe:
existing frontend code and therefore does not prescribe. Each has a
**Recommended default**; apply it unless the item is flagged as a real
business/security decision.
- **Public-key enrollment mechanism** (§2.1) — how an admin's Ed25519
`publicKeyBase64` gets associated with an account/role server-side (admin
tool? one-time enrollment link? manual DB entry?). Zero frontend code
exists for this by design.
**Recommended default:** an authenticated admin-management endpoint
(`POST /backoffice/users/{id}/public-key`), callable only by an
already-authenticated Owner/Admin — treat it as a users-management
action (§3.5), not a public/unauthenticated enrollment flow.
- **Refresh-token reuse/compromise detection** (§6) — whether presenting an
already-rotated-out refresh token should revoke the whole token family.
Not implied by any frontend behavior.
**Recommended default:** yes — revoke the whole token family on reuse of
a rotated-out refresh token (standard rotation-detection practice); this
is a security posture, not just a technical convention, so treat as
security-review-required rather than silently skippable.
- **Session/token revocation propagation** (§8) — whether/how a
remotely-revoked admin session (e.g. via the mock `AdminUsersGateway.
revokeSession`) is communicated to an already-logged-in client before its
next refresh cycle. No push/poll mechanism exists today.
**Recommended default:** accept the propagation delay (§3.21 item 9) —
don't build push infrastructure for this alone.
- **Tenant scoping of auth requests** (§10) — whether login/challenge/verify
need an explicit tenant identifier in the payload, versus relying on
request origin. Not present in any current auth payload.
**Recommended default:** rely on request origin/host (matches how
`ApiConfigService.getBaseUrl()` already resolves tenant elsewhere, §1.9)
rather than adding a redundant payload field.
- **Relationship between the two mechanisms at cutover** — replace
`adminAuthGuard` with `ed25519AuthGuard` outright, or run both and let
role/tenant config decide? Explicitly called out in the prior
`docs/AUTH.md` as "a product decision, not made here," and nothing has
changed that.
**Business decision, not technical** — ask. (This is about auth security
posture and rollout risk, not a convention.)
- **`AdminAuthService`'s reserved JWT-pair slots** (`adminToken`/
`adminRefreshToken`, §4) — whether Mechanism A is ever meant to gain its
own token pair (as the reserved-but-unused storage suggests) independent
of the Ed25519 migration, or whether that code is dead and should be
removed. Not resolved by current usage (nothing writes to it).
**Recommended default:** treat as dead/reserved-but-unused; don't build
backend support for it. Remove client-side only if a dedicated frontend
cleanup pass confirms nothing depends on the reserved slots existing.
- **`AdminRole` naming collision** (§9) — a reconciliation/rename decision
between `core/auth/models/permission.model.ts`'s string union and
`features/admin/users/models/admin-user.model.ts`'s interface; flagged,
not resolved, by this document.
**Recommended default:** same as §3.21 item 8 — keep the coarse string
union as the enforced source, treat the richer interface as display-only.
- **Fine-grained/per-domain permissions** (§9.1) — the current model is
intentionally coarse; whether a richer permission model is ever needed is
a backend/product decision.
**Recommended default:** keep it coarse (`backoffice.read`/
`backoffice.write`/`builder.read`/`builder.write`/`settings.manage`) —
don't build fine-grained permissions speculatively; the frontend has no
UI that would use finer granularity today.
- **`APP_INITIALIZER` wiring for `AuthFacade.restoreSession()`** (§5.2) — the
code comment says this should be wired in before the Ed25519 flow goes
live, but it is not wired in today. This is frontend follow-up work, not a
backend decision, but is listed here because it changes what "session
restored on refresh" means in practice until it lands.
**Recommended default:** frontend must wire this before Ed25519 goes live
in production — flag as a blocking frontend task in the cutover plan, not
something backend needs to wait on.
---
@@ -2993,9 +3123,9 @@ existing frontend code and therefore does not prescribe:
Every claim below is grounded in current source on branch `B2B`. Where the
frontend implies nothing, the item is marked **Requires backend decision**.
Auth and error contracts are owned by sibling docs — this section references
`docs/AUTHENTICATION.md` and `docs/ERROR_CONTRACT.md` rather than restating
them, and only covers the security-relevant angle.
Auth and error contracts are owned by §4 and §6 respectively — this section
references them rather than restating them, and only covers the
security-relevant angle.
### 5.1 Origin
@@ -3012,7 +3142,7 @@ The frontend calls a small, fixed set of origins, all declared in
| `http://ip-api.com/json/...` | literal in `LocationService` | external geo-IP autodetect (no key, plaintext HTTP) |
The tenant marketplace origin is chosen by **subdomain** (`TenantResolverService`,
see `docs/AUTHENTICATION.md` §10) — there is no `X-Tenant` header or tenant
see `§4 (Authentication, this document)` §10) — there is no `X-Tenant` header or tenant
path prefix. **Backend note:** the browser will make cross-origin requests to
`api.dexarmarket.ru:445` and `qr.vitanova.network` from whatever host the SPA
is served on (e.g. `dexarmarket.ru`), so those origins must return correct
@@ -3058,7 +3188,7 @@ mandatory. Requires backend decision only if that design change is made.
### 5.4 JWT
Cross-reference `docs/AUTHENTICATION.md` §3§4. Security-relevant storage
Cross-reference `§4 (Authentication, this document)` §3§4. Security-relevant storage
angle only:
- **Live today (Telegram session, Mechanism A):** no JWT at all — an opaque
@@ -3081,16 +3211,16 @@ angle only:
Refresh tokens are protected client-side only as well as `localStorage`
protects them — i.e. not from XSS (§5.4). The frontend never decodes the
refresh token (round-tripped opaque), stores it beside the access token, and
replaces the pair on every `/verify` and `/refresh` (`docs/AUTHENTICATION.md`
replaces the pair on every `/verify` and `/refresh` (`§4 (Authentication, this document)`
§4, §6). Rotation is **expected** but only the backend can enforce it. There is
no client-side reuse-detection. **Requires backend decision** (already flagged
in `docs/AUTHENTICATION.md` §6): single-use refresh tokens + reuse/compromise
in `§4 (Authentication, this document)` §6): single-use refresh tokens + reuse/compromise
revocation cascade — nothing in the frontend implies or depends on it.
### 5.6 Rate limiting
There is **no client-side rate-limit handling** (grep: no `429` / "rate limit"
reference in `src/`; confirmed by `docs/ERROR_CONTRACT.md` §429). The frontend
reference in `src/`; confirmed by `§6 (Error Model, this document)` §429). The frontend
does, however, apply **debounce** on user-driven request bursts, which implies
the natural request cadence a backend limiter should tolerate rather than block:
@@ -3102,13 +3232,13 @@ the natural request cadence a backend limiter should tolerate rather than block:
**Requires backend decision** on the rate-limit contract end to end (limits,
`Retry-After` header vs `retryAfterSeconds` body field, and whether the
frontend should auto-retry). Per `docs/ERROR_CONTRACT.md` §429, no retry
frontend should auto-retry). Per `§6 (Error Model, this document)` §429, no retry
interceptor exists — honoring 429 is net-new frontend work, not a config
change.
### 5.7 Replay protection
The Ed25519 signing flow (`docs/AUTHENTICATION.md` §2) signs the **raw
The Ed25519 signing flow (`§4 (Authentication, this document)` §2) signs the **raw
backend-issued nonce string exactly as received** — `Ed25519KeypairService.sign(nonce)`
applies no client-side framing, prefix, hashing, timestamp, or
counter (`src/app/core/auth/services/auth.service.ts` login flow;
@@ -3290,7 +3420,7 @@ admin action the UI exposes; enforcement must be entirely server-side.
The **intended** matrix comes from the dormant Ed25519 flow's `ROLE_PERMISSIONS`
table (`src/app/core/auth/models/permission.model.ts`, mirrored in
`docs/AUTHENTICATION.md` §9.1). Roles are the string union
`§4 (Authentication, this document)` §9.1). Roles are the string union
`Owner | Administrator | Editor | Support | ReadOnly`; permission domains are
`backoffice`, `builder`, `users`, `settings`:
@@ -3304,7 +3434,7 @@ table (`src/app/core/auth/models/permission.model.ts`, mirrored in
Guards that would eventually enforce this (`ed25519AuthGuard`,
`permissionGuard(permission)`) exist but are **not referenced by any route**
today (`docs/AUTHENTICATION.md` §11). The mapping is deliberately coarse —
today (`§4 (Authentication, this document)` §11). The mapping is deliberately coarse —
there is no per-domain granularity (e.g. "edit prices but not delete products")
anywhere client-side.
@@ -3315,7 +3445,7 @@ anywhere client-side.
needed (none implied client-side).
- **Requires backend decision:** the `AdminRole` naming collision (auth string
union vs the users-management `AdminRole` interface with `{id, name,
permissions[], builtIn}`) — flagged in `docs/AUTHENTICATION.md` §9; the
permissions[], builtIn}`) — flagged in `§4 (Authentication, this document)` §9; the
users-management interface suggests custom/non-builtin roles with arbitrary
permission-string sets, which the coarse 5-role union does not model. Reconcile
before building the backend role table.
@@ -3619,10 +3749,10 @@ maintenance window should send `"code": "MAINTENANCE_MODE"` (still with HTTP
status 503, since it's a byte-identical "the service is not accepting
requests" situation, but a different reason). This is the only way for the
frontend to build a distinct maintenance-mode UX later, since status alone
is not enough. `docs/MAINTENANCE_MODE.md` (sibling task, in progress) owns
the UX/copy for the maintenance case — this document only fixes the wire
signal it must key off (`error.code === "MAINTENANCE_MODE"`), so the two
docs stay consistent without duplicating UX detail here.
is not enough. §10 (Maintenance Mode) owns the UX/copy for the maintenance
case — this section only fixes the wire signal it must key off
(`error.code === "MAINTENANCE_MODE"`), consistent with §10 without
duplicating UX detail here.
#### Maintenance mode
@@ -3647,7 +3777,7 @@ should be treated as advisory-only.
**Frontend reaction today:** none — no maintenance-mode concept exists in
the frontend at all currently (confirmed: no matches for "maintenance" in
`src/`). This entire row is new; the sibling `docs/MAINTENANCE_MODE.md` task
`src/`). This entire row is new; the sibling `§10 (Maintenance Mode, this document)` task
should treat it as building from scratch, not preserving anything.
#### Tenant disabled
@@ -3728,11 +3858,9 @@ not something this contract can silently paper over.
}
```
Ties to the Ed25519 admin-auth flow documented in `AUTHENTICATION.md`
(sibling task, in progress) — keep the `code` value (`INVALID_SIGNATURE`)
consistent with whatever that doc names the failure mode, since this
contract only defines the wire shape and that doc owns the auth-flow
narrative.
Ties to the Ed25519 admin-auth flow documented in §4 — keep the `code`
value (`INVALID_SIGNATURE`) consistent with what §4 names the failure mode;
this contract only defines the wire shape, §4 owns the auth-flow narrative.
**Frontend reaction today:** same gap as "Expired token" above.
`AuthService.login()` passes `fallbackCode: 'invalid-signature'` into
@@ -3789,25 +3917,56 @@ per-status handling, except in the Ed25519 admin-auth module.
### Summary: "Requires backend decision" items
Each has a **Recommended default** — apply it unless flagged otherwise.
- **Envelope adoption** — the `{ error: { code, message, status, requestId,
details? } }` shape itself; no frontend code parses any envelope today.
**Recommended default:** adopt it as specified — every status-by-status
example in this section already uses it; it's the shape to build against.
- **401 on customer-facing marketplace calls** (`/cart`, `/orders`, etc.) —
no unified "session expired, please re-auth" UX exists for the customer
Telegram-session flow.
**Recommended default:** return the standard envelope with
`code: "UNAUTHORIZED"`; the frontend re-auth UX is a follow-up frontend
task once the backend consistently returns this, not something to block
the backend on.
- **403 tenant-mismatch vs role-mismatch** distinct copy/code.
**Recommended default:** two distinct codes (`TENANT_MISMATCH`,
`FORBIDDEN`) — the frontend can pick copy per-code once they exist;
collapsing them into one code loses information for no benefit.
- **404 vs generic-error distinct UX** on catalog/product pages (currently
identical).
**Recommended default:** return real `404` for missing resources (not a
generic `500`) — this is a backend correctness item, not a decision;
the frontend UX unification is a separate, non-blocking follow-up.
- **409 conflict handling on submit** (today only a proactive
`isSlugTaken` pre-check exists; no reactive 409 handler).
**Recommended default:** return `409` with the standard envelope on any
uniqueness/concurrency conflict; treat the proactive pre-check as
best-effort only (§3.21 item 5 — TOCTOU-prone by nature).
- **422 `details[]` → inline field-error adapter** for admin forms (the
client-side `fieldError()` convention exists but nothing feeds it from a
backend response yet).
**Recommended default:** populate `details[]` as `{ field, message }` per
invalid field on every `422` — the client-side adapter already expects
this shape, it just has nothing to consume yet.
- **429 rate-limit contract end to end** — header vs body, retry-after
value, and whether the frontend auto-retries (nothing exists today).
**Recommended default:** standard `Retry-After` header (seconds) +
envelope body with `code: "RATE_LIMITED"`; no frontend auto-retry (the
user retries manually) — simplest safe default, matches how the rest of
the app already treats errors as terminal-until-user-action.
- **Maintenance-mode `maintenanceUntil` reliability** — advisory only, or
can the frontend promise an ETA.
**Recommended default:** advisory only — word the frontend copy as
"expected back around X" not a guarantee, so an inaccurate estimate
doesn't need special-case backend handling.
- **Tenant-disabled status code and screen** — reuse `forbidden` copy (wrong
wording today) vs. add a dedicated `AuthErrorCode`.
**Recommended default:** dedicated `code: "TENANT_DISABLED"` (already
named consistently elsewhere in this doc, §1.12) rather than overloading
`forbidden` — one wrong-wording bug is enough of a reason not to add a
second overload.
- **Expired-token / invalid-signature body-code fix** — both are
**pre-existing frontend bugs**, not just missing decisions:
`toAuthErrorShape()` in `auth.service.ts` currently derives the error code
@@ -3815,6 +3974,12 @@ per-status handling, except in the Ed25519 admin-auth module.
so the `'session-expired'` and `'invalid-signature'` screens are dead code
from any real backend response today. Fixing this requires both a backend
body `error.code` and a frontend change to prefer it.
**Recommended default:** backend returns `code: "TOKEN_EXPIRED"` /
`code: "INVALID_SIGNATURE"` on the relevant `401`s; frontend fix (prefer
body code over status-derived fallback) is tracked separately in
`docs/KNOWN-ISSUES.md` and is not blocking backend work — the backend
side of this can and should ship regardless of when the frontend fix
lands.
---
@@ -4546,8 +4711,8 @@ itself is unavailable, not the whole API).
any other request failure. `ApiService` (`src/app/services/api.service.ts`) has no
per-endpoint error branching for 503; a failed `createCartPayment()`/`createOrder()`
call surfaces through whatever generic error handling the checkout components already
have for network failures (out of scope for this doc — see the sibling
`ERROR_CONTRACT.md` task for the general error-response shape).
have for network failures (out of scope for this section — see §6 for the
general error-response shape).
**No frontend UI currently exists for this — requires a future frontend task.** The
checkout flow would need a "payments unavailable" inline state (banner or disabled
@@ -4593,7 +4758,7 @@ endpoints keep working. `403 Forbidden` is arguably more correct REST semantics
+ `Retry-After` communicates "temporary" more clearly to a client and is
recommended so the frontend can offer a countdown/retry consistent with §5's pattern.
**Requires backend decision:** which status code is authoritative — this should be
pinned down jointly with whatever `ERROR_CONTRACT.md` settles on for its 5xx
pinned down jointly with whatever `§6` settles on for its 5xx
conventions, since read-only is really "a subset of write endpoints return
maintenance-503."
@@ -4603,7 +4768,7 @@ ahead of a failed request (e.g. `featureFlags.readOnly` or a dedicated
`platformStatus.readOnly` field would need to be added to `BootstrapConfig` if the
product wants a proactive banner instead of a reactive failure). Reactive handling
(showing an error when the write call 503s) can reuse the same inline
error-state pattern as §3/§6 once `ERROR_CONTRACT.md` defines the generic error body
error-state pattern as §3/§6 once `§6` defines the generic error body
handling.
---
@@ -4679,12 +4844,8 @@ richer shape from §5, scoped to `module`, not a plain `featureFlags` boolean.
All maintenance-scenario responses use HTTP `503 Service Unavailable` (except the
read-only debate in §4) with a `Retry-After` header (seconds, standard HTTP) and a
JSON body. This is written to be consistent with, not contradict, whatever
`ERROR_CONTRACT.md` (sibling task, in progress) settles on for its general
structured-error envelope — if that doc defines a different top-level error
shape (e.g. `{ error: { code, message, ... } }` vs. a flatter shape), this body
should be nested under that envelope rather than duplicating a competing shape.
Pending that reconciliation, the fields below are what the frontend needs regardless
JSON body, nested under the §6 error envelope (`{ error: { code, message, ... } }`)
rather than as a competing top-level shape. The fields below are what the frontend needs regardless
of the outer envelope:
```json
@@ -4722,7 +4883,7 @@ Per-scenario summary:
| Global | 503 | `"global"` | On every endpoint, especially `/bootstrap` |
| Per-tenant | 503 | `"tenant"` | On that tenant's `/bootstrap` and all its endpoints |
| Per-module | 503 | `"module"` | Only on that module's endpoints (e.g. `/cart`, `/orders`) |
| Read-only | 503 or 403 | `"readonly"` | Only on write endpoints; GETs unaffected — pin down with `ERROR_CONTRACT.md` |
| Read-only | 503 or 403 | `"readonly"` | Only on write endpoints; GETs unaffected — pin down with `§6` |
| Scheduled (advance notice) | 200, via `bootstrap.maintenanceNotice` | n/a | Not an error response — a proactive field on the normal `/bootstrap` payload, see §5 |
| Temporary feature disable | 200, via `bootstrap.featureFlags.<key> = false` | n/a | Not an error response — existing bootstrap mechanism, see §6 |
@@ -4771,19 +4932,35 @@ these.
### Requires backend decision (full list)
Each has a **Recommended default** — apply it unless flagged otherwise.
- §1: whether a dedicated `GET /status`/`GET /maintenance` probe should exist for
auto-recovery polling, beyond a plain 503 on `/bootstrap`.
**Recommended default:** no dedicated probe — reuse `GET /bootstrap`'s
existing 503/200 as the recovery signal; a second endpoint duplicates it.
- §2: the `scope` discriminator (`"global"` vs `"tenant"`) so the frontend can tell
the two apart from a single tenant's bootstrap response; and whether a
disabled tenant's branding can still resolve for a branded takeover page.
**Recommended default:** include `scope` in the 503 body (§7 shape);
serve branding for a disabled-but-known tenant so the takeover page can
still be branded (only truly unknown tenants get the unbranded default).
- §3: none beyond adopting the §7 response shape per-endpoint — this one is mostly
frontend-gap, not backend-undecided.
- §4: which status code is authoritative for read-only (`503` vs `403`) — to be
pinned down jointly with `ERROR_CONTRACT.md`.
pinned down jointly with §6.
**Recommended default:** `503` with `code: "READ_ONLY"` — it's a temporary
service-state condition, not a permissions failure, so it belongs in the
5xx family alongside the rest of maintenance mode, not 4xx.
- §5: whether scheduled-maintenance notice ships via a `bootstrap.maintenanceNotice`
field (proposed) or a separate polling endpoint.
**Recommended default:** the `bootstrap.maintenanceNotice` field — it
rides the existing bootstrap fetch/cache lifecycle for free; a separate
polling endpoint adds a new recurring request for no added value.
- §7: how this document's 503 body nests inside whatever outer envelope
`ERROR_CONTRACT.md` defines.
§6 defines.
**Recommended default:** already resolved — nest under the §6 envelope
(`{ error: { code, message, status, requestId, ... } }`) as shown in this
section's JSON examples; no separate top-level shape.
### No frontend UI currently exists for this — requires a future frontend task (full list)