Added 2026-08-18: two levels now sit **above**`Marketplace`, introduced by [PARTNER-PROVISIONING-API-CONTRACT.md §10](PARTNER-PROVISIONING-API-CONTRACT.md).
```ts
interface Company {
id: string;
name: string;
externalReference?: string; // partner's own id, when provisioned via the partner API
status: 'active' | 'suspended' | 'disabled';
createdAt: string;
updatedAt: string;
}
interface Project {
id: string;
companyId: string;
name: string; // a product line, e.g. "marketplaces"
externalReference?: string;
status: 'active' | 'suspended' | 'disabled';
createdAt: string;
updatedAt: string;
}
```
Both are deliberately thin — they exist to scope ownership, credentials, and payment routing, not to hold configuration. All marketplace configuration stays on `Marketplace` below.
A `Marketplace`**is** the partner hierarchy's `store` level. One project holds many marketplaces; one marketplace holds many sellers (Phase 5), and sellers are not part of that hierarchy.
supersedesRevisionId?: string; // rollback creates a NEW revision, never mutates the old one
}
```
**Hard invariant:** `Order`, `Payment`, `InventoryRecord`, and every financial ledger row are **not part of a `MarketplaceRevision`**. Rolling back a storefront design revision must never touch commerce data.
Added 2026-08-18. The leaf of the partner hierarchy: one payment method accepted at one marketplace. A marketplace taking both QR and card has two payment points.
```ts
interface PaymentPoint {
id: string;
marketplaceId: string;
method: 'qr' | 'card'; // extensible; both ship today
currencies: string[]; // ISO 4217 subset this channel accepts
externalReference?: string;
status: 'active' | 'suspended' | 'disabled';
providerAccountRef?: string; // set only by financial enablement, never by provisioning
createdAt: string;
updatedAt: string;
}
```
- Creating a payment point registers the channel. It does **not** enable real money — that requires `providerAccountRef`, set through a separate approved flow.
- A payment point is what `RoutingContext.leafNodeId` points at (Phase 1 §6.5).
-`MarketplaceFeatureSet.features.payments` gates whether the marketplace may have enabled payment points at all; the payment point gates which method.
### 1.2 Backfill
Existing marketplaces predate `Company` and `Project`. Migration, in this order:
```
1. Create one Company for the current owning entity.
2. Create one Project ("marketplaces") under it.
3. Set companyId + projectId on every existing Marketplace.
4. Create PaymentPoints for the methods each marketplace already accepts (qr, card).
5. Make companyId and projectId non-nullable only after 3 completes.
```
`externalReference` stays null for backfilled rows — it is only meaningful for partner-provisioned nodes.
POST /api/admin/v2/marketplaces/{id}/staging-launch -- step 7, runs smoke tests
POST /api/admin/v2/marketplaces/{id}/production-launch -- step 8, requires all P0 blockers closed + explicit approval
```
## 4. Domain automation (Hostinger API, per plan §8.2)
```
GET /api/dns/v1/zones/{domain}
POST /api/dns/v1/zones/{domain}/validate
PUT /api/dns/v1/zones/{domain}
DELETE /api/dns/v1/zones/{domain}
GET /api/dns/v1/snapshots/{domain}
GET /api/dns/v1/snapshots/{domain}/{snapshotId}
POST /api/dns/v1/snapshots/{domain}/{snapshotId}/restore
```
Process, strictly in this order:
```
1. Read current DNS zone.
2. Save a snapshot (rollback payload) BEFORE any change.
3. Build and validate a DNS plan.
4. NEVER touch MX/SPF/DKIM/DMARC/CAA records without a separate, explicitly scoped task.
5. Apply records only after production approval.
6. Verify propagation, SSL issuance, and health checks.
7. Mark the domain 'active' only after all checks in step 6 pass.
```
## 5. Publish model
```
draft -> validation -> preview -> publish
```
```
POST /api/admin/v2/marketplaces/{id}/revisions -- create draft
POST /api/admin/v2/marketplaces/{id}/revisions/{revId}/validate
POST /api/admin/v2/marketplaces/{id}/revisions/{revId}/publish -- becomes immutable
POST /api/admin/v2/marketplaces/{id}/revisions/{revId}/rollback -- creates a NEW revision pointing at the prior published content
```
Replaces the current builder's `localStorage`-only draft persistence and the empty `apiEndpoints.builder: {}` placeholder in bootstrap. CMS/static-page content (currently in-memory bootstrap only) gets a real write path through this same revision model.
Added 2026-08-21 (FH-2.7). §5 says a published revision "becomes immutable" and that rollback creates a new revision. The details that make that true:
-`version` is an integer, assigned as `max(version) + 1` for the marketplace, inside the publishing transaction. `UNIQUE (marketplaceId, version)`.
- A revision row stores the **materialized** snapshot — the effective content and configuration at publish time, not references that can later resolve differently. A product renamed tomorrow does not retroactively change what was published today.
- Publishing flips a single `publishedRevision` pointer in the same transaction that writes the snapshot. There is no window in which a marketplace is serving a half-published state.
- Rollback reads revision *n*, writes it as revision *max+1*, and points at that. Revision *n* is untouched. History only grows.
- **Operational state does not travel with a revision.** Inventory, reservations, orders and payments are live data. Rolling back last week's design must not roll back this week's stock. This is worth stating because it is the single most tempting shortcut in a revision system and the most expensive one to discover in production.
### 5.2 Clone
Added 2026-08-21 (FH-2.7). Launching marketplace *n+1* from an existing one is the platform's core promise, so what a clone does and does not carry is a contract, not an implementation choice.
Carried: theme and design configuration, sections, pages, navigation, category tree, collections, and offer assignments.
**Not** carried, under any flag: domains, admin users and memberships, customers, customer sessions, orders, payments, payment credentials, webhook secrets, audit history.
Inventory in the clone starts at zero unless a platform-scope role explicitly opts otherwise. Cloning stock by default means a new storefront can sell units that a different storefront is holding.
The category tree is copied by a topological walk with explicit cycle detection — a cycle is a `400` naming the offending categories, never an infinite loop and never a silently truncated tree.
### 5.3 Preview is signed and read-only
Added 2026-08-21 (FH-2.6). We have preview in the product and no preview safety anywhere in these contracts.
```
POST /api/admin/v2/marketplaces/{id}/preview-token -> { url, expiresAt }
```
- The token is an HMAC signature over `{ marketplaceId, expiresAt, nonce }`, TTL 15 minutes, delivered as an `HttpOnly` cookie scoped to the preview host. Signature comparison is constant-time; an invalid or expired token is `404`, not `401` — an unpublished storefront should not confirm its own existence.
- **While a preview cookie is present, every non-`GET` on the public API returns `404`.** Enforced by a hook ahead of routing, not per endpoint. Preview exists to look at an unpublished storefront, never to transact against one — otherwise preview becomes a way to place real orders and move real stock against a design nobody approved.
Added 2026-08-21 (FH-2.5), the parts that decide whether the two rules above actually hold:
- **Normalization is specified, not assumed:** lowercase, strip a trailing dot, strip the port, then match. `Shop.Example.COM.:443` and `shop.example.com` are one tenant. A normalization that differs between the lookup and the domain-verification write is a tenant-isolation bug.
- **A domain row only resolves once `verifiedAt` is set** and the marketplace is in a serving state. An unverified domain is `404`, so pointing DNS at us is not by itself enough to make someone else's brand serve.
- Host lookups may be cached briefly (~30 s) — with **explicit invalidation** on domain add, verify, remove, and marketplace state change. Without invalidation, a suspended marketplace keeps serving for the length of the cache, which is the wrong side to fail on.
-`Host` is read from the verified proxy header chain, with the proxy configured to overwrite rather than append what the client sent. A client-supplied `Host`/`X-Forwarded-Host` is not evidence.
- **No public endpoint accepts a `marketplaceId`** in path, query, or body. If one does, the Host check is decoration.
**Acceptance:** a request with an unknown or unverified Host returns 404 and no data belonging to any other tenant.