# Phase 9 Backend Contract — Tenant Registry, Domain Automation, Publish Model Companion to [PRODUCT-PLAN-v3.1-DELIVERY-PLAN.md](../PRODUCT-PLAN-v3.1-DELIVERY-PLAN.md) Phase 9 (Sprints 9.1–9.3). Covers plan §4.3, §8. **Status: ready to build.** Zero `hostinger` references exist in the codebase today. --- ## 1. Entities 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. ```ts interface Marketplace { id: string; companyId: string; // added 2026-08-18 projectId: string; // added 2026-08-18 externalReference?: string; // added 2026-08-18, partner's own id for this store name: string; code: string; type: 'commerce' | 'mall_directory' | 'hybrid' | 'single_brand'; ownerId: string; countries: string[]; locales: string[]; currencies: string[]; timezone: string; lifecycleState: MarketplaceLifecycleState; } type MarketplaceLifecycleState = | 'draft' | 'configured' | 'content_ready' | 'domains_planned' | 'staging_live' | 'qa_passed' | 'production_ready' | 'live' | 'paused' | 'archived'; interface MarketplaceDomain { marketplaceId: string; domain: string; type: 'production' | 'www' | 'staging' | 'preview' | 'api' | 'seller'; status: 'planned' | 'dns_pending' | 'ssl_pending' | 'active' | 'failed'; } interface MarketplaceFeatureSet { marketplaceId: string; features: Record; // e.g. { catalog: true, sellers: true, cart: true, checkout: true, payments: true, orders: true, refunds: true, directory: false, ... } } interface MarketplaceRevision { id: string; marketplaceId: string; status: 'draft' | 'validated' | 'preview' | 'published'; publishedAt?: string; 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. ### 1.1 PaymentPoint 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. ## 2. Lifecycle state machine ``` draft -> configured -> content_ready -> domains_planned -> staging_live -> qa_passed -> production_ready -> live -> paused/archived ``` Every state transition endpoint must return the specific blocker preventing the next transition — not just "not ready." ``` GET /api/admin/v2/marketplaces/{id}/lifecycle -> { currentState, nextState, blockers: string[] } POST /api/admin/v2/marketplaces/{id}/lifecycle/advance ``` ## 3. Onboarding wizard (8 steps, plan §4.3) ``` POST /api/admin/v2/marketplaces -- step 1: name/code/type/owner/countries/locales/currencies/timezone PATCH /api/admin/v2/marketplaces/{id}/feature-set -- step 2 POST /api/admin/v2/marketplaces/{id}/domains -- step 3 PATCH /api/admin/v2/marketplaces/{id}/design -- step 4 POST /api/admin/v2/marketplaces/{id}/roles -- step 5 PATCH /api/admin/v2/marketplaces/{id}/integrations -- step 6 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. ## 6. Tenant resolution hardening ``` GET /api/v2/storefront/bootstrap -- resolved server-side from verified Host header ``` - Host is normalized and matched against `MarketplaceDomain` server-side — the marketplace ID from the browser is never a trust boundary. - Unknown Host → `404`, with **no fallback to any other tenant**. ## 7. What the frontend will start doing once this ships - Build the backoffice **Marketplaces** section (missing from admin nav today): registry, type, status, domains, currencies, feature set, responsible manager. - Build the **Domains & Releases** section: DNS/SSL status, staging/production, health checks, rollback. - Wire the project editor/builder to real revision persistence instead of `localStorage`. - Marketplace dashboard: GMV, paid orders, conversion, payment failure rate, moderation queue, low stock, unmatched events, integration health, domain/SSL/release status (plan §4.2).