Writes the 14 harvested mechanisms from FORK-ANALYSIS-2026-08-21.md into
the backend contracts. Each section is dated 2026-08-21 and tagged FH-*
so any wording traces back to why it is worded that way.
The through-line: several contracts stated correctness as behaviour
("the webhook must be idempotent"). Behaviour written as an if-statement
gets deleted by a refactor and the failure mode is a double charge. These
sections restate it as schema and mechanism.
PHASE-3 3.1 conditional-write reservation, 409 on zero rows, cart-wide
rollback, 15 min TTL
3.2 InventoryMovement append-only journal with resultingAvailable
6 bulk import idempotent by SKU, rollback while unsold
6a digital code pools, revealed only when paid
PHASE-7 5 unique constraints for payment idempotency and webhook
replay, insert-first handling, signature over raw body,
24h poll as reconciliation not primary
TRACK-S 2.1 session model - 32 bytes stored as SHA-256 only, HttpOnly,
one cookie per contour, Argon2id params, mandatory TOTP
2.2 origin allowlist ahead of routing on every cookie mutation
4.2 AES-256-GCM envelope for stored secrets, HMAC fingerprints
8a order manager as a separate contour, scoped by membership
rows rather than by configuration
PHASE-9 5.1 revision immutability, version = max+1, pointer flipped
in-transaction, operational state does not travel
5.2 clone carry / no-carry list, inventory to zero
5.3 signed read-only preview, non-GET 404s while previewing
6 host normalization, verifiedAt required, cache invalidation
PHASE-10 3a server re-runs the editor's validation, clamp-and-fallback
PHASE-2 3.1 order publicToken, snapshot completeness, never updated
FH-2.12 rejected on the merits: our marketplace lifecycle state machine
is richer than theirs, adopting it would be a downgrade. Recorded in the
TODO so it is not raised again.
Also adds BACKEND-HANDOFF.md sections 0 and 0a - nine falsifiable
invariants as a release gate, each cross-referenced to the contract that
specifies it, plus PR and release discipline. And ADR-0006 recording what
we take, what we reject, what we keep because ours is better, and the
organizational question it deliberately does not settle.
No implementation changes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
13 KiB
Backend handoff — start here
Single entry point for a backend developer picking this up cold. Written 2026-08-18.
0. Invariants — the acceptance gate
Added 2026-08-21 (FH-E.1). Nine statements. Each is falsifiable, each has a test, and a release that violates any of them does not ship regardless of what else is finished. They come before the contracts because a contract can be read selectively and these cannot.
- The public tenant is determined by verified
Hostalone. No public endpoint accepts amarketplaceIdfrom the browser. - The price of an order is computed by the backend. A price arriving in a request is ignored, never validated-and-used.
- Stock and reservation change atomically. Two buyers racing for the last unit produce one payable order.
- Payment creation and webhook receipt are idempotent, enforced by unique constraints rather than by handler logic.
- Provider credentials never leave the backend — not in a response, not in a bundle, not in a log.
- A published revision is immutable. Rollback creates a new revision; history is never rewritten.
- Rolling back design does not roll back live inventory, orders, or payments.
- No user reads a marketplace they are not assigned to — through the UI or through a direct API call.
- Every administrative mutation leaves an audit record naming actor, action, before and after.
Where each is specified: 1 and 6–7 in Phase 9 §5–6; 2 in Phase 1 §5; 3 in Phase 3 §3.1; 4 in Phase 7 §5; 5, 8 and 9 in Track S §2, §3, §4.2.
0a. How work lands
Added 2026-08-21 (FH-E.2, FH-E.3).
One functional area per pull request. A PR carries: what it does, screenshots where there is UI, API changes, migrations, test evidence, security impact, and a rollback plan. Never change a payment, inventory, or order state machine in the same PR as a redesign — those two things fail differently and must be revertable separately.
Migrations are expand/contract. A migration and the frontend change that depends on it belong to one release train, but they may be separate PRs, and the expand step must be deployable on its own.
A release is not "the build passed." Each one records: version, migrations applied, healthcheck result, post-deploy smoke result, production dependency audit, and the rollback path actually available. A local build succeeding and a UI existing are not evidence of production readiness — the deploy and the smoke check are.
1. What this is
marketplaces is a multi-tenant marketplace platform frontend (Angular 22). The frontend is built and waiting; there is no backend yet. Every wire contract the backend needs to implement is already written and sitting in this directory — see README.md for the full index and build order.
Contract sections dated 2026-08-21 and tagged FH-* come from FORK-ANALYSIS-2026-08-21.md — a review of a parallel platform implementation that has a working backend. They are mechanisms that implementation already proved, harvested deliberately; the tag is there so you can trace any one of them back to why it is worded the way it is.
1a. Multi-tenancy — the thing that shapes every endpoint
The final executable infrastructure/backend contract is TENANT-API-DOMAIN-HANDOFF.md. Follow it for hostname normalization, CORS, nginx, TLS, CI secrets, and acceptance checks.
One deployed bundle serves every customer domain. There is no per-tenant build. The chain is:
TenantResolverServicereads the complete current browser hostname and protocol (localhost still uses the development proxy).ApiConfigServiceuses one API host per base domain: bothexample.comandstore1.example.comuseapi.example.com.ApiBootstrapProvider, auth, legacy API calls, and versioned/api/...calls all use that same base.- Each base domain owns one API DNS/TLS/reverse-proxy entry. nginx validates the browser origin and forwards the complete storefront hostname as
X-Storefront-Host. - Backend tenant lookup uses that trusted storefront hostname, not the shared API
Host; frontend nginx remainsdefault_server/server_name _, so any attached storefront domain receives the same bundle.
What this means for you: the bootstrap endpoint is the single most important thing to build after auth. Every request must be tenant-scoped server-side, and a tenant must never be able to read another tenant's data — return 403, not an empty result (see TRACK-S §2). The frontend supplies the tenant identity from the hostname; the backend must treat that as an untrusted hint and derive real scope from the authenticated session.
Constraints already fixed by the frontend design (see the platform-vision facts in docs/context/): no marketplace-specific code or hardcoded marketplace data in the frontend; bootstrap carries only what is needed before app start (branding, languages, homepage layout, navigation, enabled widgets, footer pages) and never products, orders, cart, or users.
PHASE-9 covers the marketplace registry, domain attachment, and publish/revision model.
2. Read in this order
- README.md — index of all contracts, build order, and what's deliberately excluded.
- PHASE-1-MONEY-FX-PAYMENTS-CONTRACT.md — start here. Everything after depends on the money model.
- Phases 2→4 — the rest of the launch gate (orders, catalog, connectors).
- TRACK-S-SECURITY-RBAC-CONTRACT.md — gates the launch. Today the admin role model is decorative: nothing server-side enforces any permission. §8 covers per-marketplace bootstrap admin accounts and self-service sub-admin management.
- TRACK-A-ANALYTICS-CONTRACT.md — longest lead time, start it in parallel with Phase 1.
- Phases 5→10 — post-launch-gate.
- PARTNER-PROVISIONING-API-CONTRACT.md — the inbound partner API. Read it before implementing Phase 1, not after: it adds
RoutingContexttoCheckoutSession/PaymentIntent/Payment(Phase 1 §6.5) and two levels aboveMarketplace(Phase 9 §1). Building the partner API itself can wait; carrying its routing dimension in the payments tables cannot.
../../BACKEND-API-REFERENCE.md documents the current live API surface (legacy endpoints, error envelope, mock-only areas). New endpoints use /api/v2/... namespaces; legacy endpoints are not being migrated.
3. Auth — read before writing any endpoint
Auth is no longer part of this repo. It lives in @marketplaces/auth, published from vitanovaPackages. See ../PACKAGES-USAGE.md for the full client surface. What matters on the backend side:
Two mechanisms exist client-side.
- Telegram QR/session (live). Endpoints under
{authApiUrl}/users/sessions—POSTto create,GET /{id}to poll,DELETE /{id}to log out. Both customer and admin login call the same endpoints; only client-side storage differs. The response shape is normalized permissively client-side (many key spellings accepted), but a clean implementation should return{ webSessionID, user: { userId, username, firstName, lastName }, status, expiresAt }. - Ed25519 challenge/response (not built).
GET /api/admin/auth/challenge,POST /api/admin/auth/verify,POST /api/admin/auth/refresh,POST /api/admin/auth/logout. Contracts in TRACK-S and the package'sed25519/models/auth-api.model.ts. Until these ship, the client shows abackend-unavailablescreen — nothing is mocked.
The critical gap: the session API has no concept of "admin." The frontend cannot distinguish an admin session from a customer one — it only chooses where to store the result. Every admin endpoint must independently verify authorization server-side. Client-side guards are UI convenience, never security. This is the single most serious open issue in the system.
Admin requests carry AdminWebSessionID: <sessionId> (and Authorization: Bearer <token> once admin JWTs exist) on paths containing /admin/, /backoffice/, /builder/, /media/.
4. Environment / infrastructure state
Dev server 213.21.246.138 (user seto, sudo, SSH key provided separately).
| Thing | State |
|---|---|
| nginx 1.24 | Installed, running. Config at /etc/nginx/sites-enabled/marketplaces-dev.conf. Serves frontend from /srv/marketplaces/current/frontend, backoffice from /srv/marketplaces/current/backoffice, proxies /api/ → 127.0.0.1:8080. /health returns ok. |
| Go toolchain | Installed (/usr/local/bin/go). |
| Backend service on :8080 | Not running. Nothing is listening. /srv/marketplaces/current/api is an empty shell. nginx's /api/ proxy currently 502s. |
| PostgreSQL | Installed but inactive. Needs starting, a database, a user, and schema before anything works. |
| Shared packages | @marketplaces/auth installs over plain git from a release branch — no registry, token, or tunnel needed. npm install works out of the box. |
| Verdaccio (npm registry) | Running in Docker on port 4873, but superseded and unused — nothing depends on it. See ../PACKAGE-EXTRACTION.md §5. |
| Firewall (ufw) | Active. 80/tcp, 443/tcp, OpenSSH. |
| TLS / certbot | Not installed. No certificates. Everything is plain HTTP today. For multi-tenant this is real work: every customer domain needs a certificate (per-domain issuance, or a wildcard if all tenants sit under one apex). |
| DNS / dynamic subdomains | Not set up. No domain currently points at the server (reverse DNS is the provider default silky-bronze.ptr.network). No wildcard record, no per-tenant subdomain automation, no Hostinger DNS integration. The application is fully multi-tenant (§1a) — this is the missing infrastructure underneath it. PHASE-9 specifies the target. |
| Frontend deploy (CD) | None. Pushing to main deploys nothing. architecture-governance.yml builds and checks boundaries but has no deploy step, and nothing writes to /srv/marketplaces/current/frontend. Deploys are manual today. |
| CI runner | None on this server; sources.vitanova.network CI runs elsewhere. |
5. To get a working dev environment
Nothing here is done yet — this is the setup a backend dev does on day one.
- Start and configure PostgreSQL; create the database and application user.
- Design the schema from the Phase 1–4 contracts (schema design is explicitly the backend's own call — the contracts specify entities, endpoints, and invariants, never tables). Tenant scoping belongs in the schema from day one; retrofitting it is painful.
- Build the API service, listen on
127.0.0.1:8080. nginx already proxies/api/to it. - Implement the bootstrap config endpoint (§1a) — without it the frontend cannot render for any tenant.
- Implement the Telegram session endpoints — the login flow is fully built client-side and blocked only on these.
- Implement
GET /api/identity/v1/session/permissions(TRACK-S §2) — frontend route guards derive from it. - Seed per-marketplace bootstrap admins (TRACK-S §8): login = marketplace slug, password =
{slug}2026$,mustChangePassword: true.
Steps 4–6 unblock the entire frontend. Everything after is feature work.
6. Frontend deploy
git clone <marketplaces repo>
npm install # pulls @marketplaces/auth over git, no credentials needed
npm run build # -> dist/dexarmarket
Angular 22, Node 24+. nginx serves /srv/marketplaces/current/frontend.
deploy.yml builds and atomically deploys
pushes to main; one deployment updates every domain at once. Before activation,
the workflow reconciles TLS, exact CORS, and reverse proxying for every host in
STOREFRONT_DOMAINS. Production deployment requires the documented CI secrets
and the one-time server-setup.sh run.
7. Known open decisions
- Registry reachability for CI (reverse proxy + TLS, or a different registry entirely).
Backend ownership.Answered 2026-08-18: implemented by a separate backend developer against this contract set.- Additional payment providers (wallets, BNPL) — Phase 7 §4.
- Per-connector marketplace adapters — written per partner at onboarding, Phase 4 §8.
- Backfill of
Company/Project/PaymentPointfor existing marketplaces — sequence specified in Phase 9 §1.2, not yet scheduled.