Files
marketplaces/docs/BRAND-BOOTSTRAP.md
sdarbinyan c2a56571af
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
Deploy Frontend / deploy (push) Has been cancelled
feat(bootstrap): fall back to built-in placeholder when marketplace unpublished
Adds published: boolean to the bootstrap wire contract. ConfigService
swaps to a new DEFAULT_BOOTSTRAP constant (all feature flags on, generic
branding/theme/pages) whenever the backend reports published: false, so
an unpublished marketplace renders a working demo instead of a blank or
broken page. Missing published field stays backward compatible (treated
as true). Documents the brand bootstrap wire shape for backend/ops use.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-22 22:02:42 +04:00

18 KiB
Raw Permalink Blame History

Brand bootstrap — full JSON reference

What one JSON document must contain to turn this codebase into a live, branded marketplace. Frontend is Angular 22, multi-tenant, one bundle for every domain — a brand is 100% config, zero code or rebuild. Source of truth for wire shape: bootstrap-config.model.ts and its per-section models in the same folder. Backend contract: BACKEND-INTEGRATION.md. Deploy/domain/TLS mechanics: DEPLOYMENT.md.

How it works

  1. Request arrives at https://<any-domain>.
  2. nginx forwards the verified Host to the API as X-Storefront-Host. Tenant identity comes only from this header — never from a client-supplied field.
  3. SPA calls GET /bootstrap (also proxied through api.<base-domain>).
  4. Backend resolves tenant from the host, returns this JSON. Frontend renders entirely from it — theme, nav, pages, feature flags, locales.
  5. One backend, many brands: each Marketplace row + its MarketplaceDomain rows is a brand. No per-brand deploy.

Acceptance check used in CI: curl -fsS https://api.<domain>/bootstrap | jq -e 'type=="object"'.

Minimal path to a new brand

  1. Backend: create a Marketplace row (docs/backend/BACKEND-INTEGRATION.md §11) and at least one MarketplaceDomain (type: 'production').
  2. Point the domain's DNS A record at the server.
  3. TLS: either it's a *.yourapex.com subdomain (wildcard, zero extra work — DEPLOYMENT.md §4.1) or a customer's own domain (add-domain.sh, §4.4, or the sync-domains.sh reconciler, §4.2).
  4. configure-api-domain.sh for the base domain — creates api.<domain> (backend proxy, CORS, cert). One API hostname per base domain; subdomains reuse it.
  5. Backend returns a populated bootstrap JSON for that Host. Nothing to redeploy on the frontend side.
  6. Verify: curl -I https://<domain>/health (nginx, expect 200) and curl -fsS https://api.<domain>/bootstrap | jq . (backend, expect the object below).

Full annotated example

{
  "schemaVersion": "1.0.0",
  "generatedAt": "2026-08-22T00:00:00Z",

  "tenant": {
    "id": "tenant-acme-001",
    "slug": "acme",
    "code": "ACME",
    "host": "shop.acme.com",
    "name": "Acme Marketplace",
    "websiteBaseUrl": "https://shop.acme.com",
    "builderBaseUrl": "https://builder.shop.acme.com",
    "backofficeBaseUrl": "https://backoffice.shop.acme.com",
    "defaultLocale": "en",
    "supportedLocales": ["en", "ru"],
    "defaultCurrency": "USD",
    "supportedCurrencies": ["USD", "EUR"],
    "timezone": "America/New_York",
    "documentationUrl": "https://docs.shop.acme.com"
  },

  "branding": {
    "brandName": "Acme",
    "legalName": "Acme Commerce LLC",
    "slogan": "Everything, delivered",
    "logoUrl": "https://cdn.acme.com/logo.svg",
    "logoCompactUrl": "https://cdn.acme.com/logo-compact.svg",
    "faviconUrl": "https://cdn.acme.com/favicon.ico",
    "appIconUrl": "https://cdn.acme.com/icon-192.png",
    "supportEmail": "support@acme.com",
    "supportPhone": "+1-555-000-0000"
  },

  "theme": {
    "themeId": "acme-light",
    "mode": "light",
    "palette": {
      "primary": "#1a56db",
      "secondary": "#7e8a97",
      "accent": "#60a5fa",
      "success": "#10b981",
      "warning": "#f59e0b",
      "danger": "#ef4444",
      "info": "#3b82f6",
      "textPrimary": "#111827",
      "textSecondary": "#6b7280",
      "backgroundPrimary": "#ffffff",
      "backgroundSecondary": "#f9fafb",
      "border": "#e5e7eb"
    },
    "typography": {
      "primaryFontFamily": "Inter, sans-serif",
      "headingFontFamily": "Inter, sans-serif",
      "baseFontSize": 16
    },
    "spacing": { "unit": 4, "scale": [0, 4, 8, 12, 16, 24, 32, 48] },
    "borderRadiusScale": { "sm": "6px", "md": "10px", "lg": "14px", "xl": "20px" },
    "shadows": {
      "sm": "0 2px 8px rgba(0,0,0,0.1)",
      "md": "0 4px 12px rgba(0,0,0,0.15)",
      "lg": "0 12px 32px rgba(26,86,219,0.2)"
    },
    "iconSet": "default"
  },

  "company": {
    "companyName": "Acme Commerce LLC",
    "registrationNumber": "0000000000",
    "taxId": "00-0000000",
    "address": {
      "country": "USA",
      "region": "NY",
      "city": "New York",
      "street": "5th Ave 1",
      "postalCode": "10001"
    },
    "contacts": {
      "email": "support@acme.com",
      "phone": "+1-555-000-0000",
      "telegram": "@acme_support",
      "website": "https://acme.com"
    }
  },

  "featureFlags": {
    "wishlist": true, "compare": true, "reviews": true, "blog": false,
    "chat": false, "analytics": true, "notifications": true,
    "coupons": true, "loyalty": false, "giftCards": false, "invoices": true
  },

  "apiEndpoints": {
    "bootstrap": { "path": "/bootstrap", "method": "GET", "timeoutMs": 10000 },
    "website": {}, "builder": {}, "backoffice": {}
  },

  "localization": {
    "defaultLocale": "en",
    "supportedLocales": ["en", "ru"],
    "currencyByLocale": { "en": "USD", "ru": "RUB" },
    "dictionaries": [
      { "locale": "en", "dictionaryUrl": "/assets/i18n/en.json", "version": "1.0.0" },
      { "locale": "ru", "dictionaryUrl": "/assets/i18n/ru.json", "version": "1.0.0" }
    ]
  },

  "seo": {
    "default": { "title": "Acme", "description": "Everything, delivered", "robots": "index,follow" },
    "byPageKey": {
      "home": { "title": "Acme - Home", "description": "Everything, delivered", "canonicalUrl": "https://shop.acme.com/", "robots": "index,follow" }
    }
  },

  "permissions": {
    "definitions": [
      { "key": "builder.pages.edit", "description": "Edit pages in builder" },
      { "key": "backoffice.products.read", "description": "Read products in backoffice" }
    ],
    "roles": [
      { "role": "builder_admin", "permissions": ["builder.pages.edit"] },
      { "role": "backoffice_manager", "permissions": ["backoffice.products.read"] }
    ]
  },

  "header": { "showLogo": true, "showSearch": true, "showCategories": true, "showCart": true, "sticky": true, "layout": "default" },
  "layout": { "type": "default" },

  "navigation": {
    "header": [
      { "id": "nav-home", "labelKey": "nav.home", "route": "/", "icon": "home", "order": 1 },
      { "id": "nav-search", "labelKey": "nav.search", "route": "/search", "icon": "search", "order": 2 },
      { "id": "nav-cart", "labelKey": "nav.cart", "route": "/cart", "icon": "cart", "order": 3 }
    ],
    "footer": [
      { "id": "footer-about", "labelKey": "nav.about", "route": "/about-us", "order": 1 },
      { "id": "footer-privacy", "labelKey": "nav.privacy", "route": "/privacy-policy", "order": 2 }
    ]
  },

  "footer": {
    "paymentIcons": [{ "src": "/assets/images/visa-logo.svg", "alt": "Visa", "width": 40, "height": 28 }],
    "copyrightText": { "en": "© 2026 Acme. All rights reserved.", "ru": "© 2026 Acme. Все права защищены." },
    "legalPageKeys": ["about-us", "privacy-policy", "terms-of-service"]
  },

  "catalog": {
    "layout": "grid",
    "navigationMode": "default",
    "defaultSort": "relevance",
    "availableSorts": ["relevance", "latest", "price_asc", "price_desc", "rating", "popular", "discount"],
    "enabledFilters": ["price", "availability", "rating", "brand", "category"],
    "showBreadcrumbs": true, "showCategoryBanner": true, "showRatings": true,
    "showDiscounts": true, "showAvailability": true, "suggestionsEnabled": true, "searchHistoryEnabled": true
  },

  "productPage": {
    "rating": { "enabled": true },
    "reviews": { "enabled": true, "pageSize": 5, "showSummary": true },
    "questions": { "enabled": true, "pageSize": 5 },
    "tabs": { "enabled": true, "items": ["description", "specifications", "reviews", "questions", "delivery", "warranty"] },
    "relatedProducts": { "enabled": true }
  },

  "userExperience": {
    "wishlist": { "enabled": true, "headerBadgeEnabled": true },
    "compare": { "enabled": true, "maxItems": 4, "hideIdenticalDefault": false, "highlightDifferencesDefault": true },
    "recentlyViewed": { "enabled": true, "maxItems": 12, "widgetEnabled": true },
    "share": { "enabled": true },
    "continueBrowsing": { "enabled": true },
    "savedSearches": { "enabled": true, "maxItems": 10 }
  },

  "features": {
    "wishlist": true, "compare": true, "reviews": true, "comments": true,
    "questions": true, "recommendations": true, "recentlyViewed": true,
    "searchHistory": true, "recentlySearched": true, "ratings": true,
    "share": true, "brands": true, "manufacturers": true,
    "availability": true, "discounts": true, "badges": true
  },

  "widgetRegistry": { "manifestUrl": "https://api.acme.com/widget-manifest.json" },

  "staticPages": {
    "about-us": {
      "route": "/about-us",
      "title": { "en": "About Us", "ru": "О компании" },
      "html": { "en": "<h2>About Us</h2><p>...</p>", "ru": "<h2>О компании</h2><p>...</p>" }
    }
  },

  "pages": [
    {
      "id": "page-home", "key": "home", "title": "Home",
      "route": { "path": "/", "exact": true },
      "layout": { "type": "default" },
      "seoKey": "home", "visible": true,
      "sections": [
        {
          "id": "section-hero", "type": "hero", "order": 1,
          "layout": { "strategy": "hero", "columns": 1, "gap": "1.5rem", "align": "stretch" },
          "visibility": { "desktop": true, "tablet": true, "mobile": true },
          "visible": true,
          "widgets": [
            {
              "id": "widget-hero-main", "type": "hero", "version": "1.0.0", "order": 1,
              "padding": "0.5rem 0",
              "visibility": { "desktop": true, "tablet": true, "mobile": true },
              "visible": true,
              "props": {
                "title": { "en": "Welcome to Acme", "ru": "Добро пожаловать в Acme" },
                "subtitle": { "en": "Everything, delivered", "ru": "Всё, с доставкой" },
                "ctaLabel": { "en": "Start Shopping", "ru": "Начать покупки" }
              }
            }
          ]
        }
      ]
    }
  ],

  "modules": { "sellerManagement": { "enabled": false } }
}

Field reference

tenant (required) — tenant.model.ts

Identity and locale/currency defaults. host must exactly match the domain nginx forwards — mismatches are how tenant leakage bugs happen. websiteBaseUrl / builderBaseUrl / backofficeBaseUrl are the three surfaces this same brand can present (storefront, page builder, admin backoffice) — each gets its own subdomain or host.

branding (required) — branding.model.ts

Everything a human sees as "this is the brand": name, logo variants, favicon, support contact. logoCompactUrl is used where header space is tight (mobile, collapsed nav).

theme (required) — theme.model.ts

Full design-token set: color palette, typography, spacing scale, border radii, shadows. Consumed by theme-css-vars.mapper.ts → CSS custom properties at runtime. mode is light or dark; ship a matching palette for whichever themeId you pick.

company (required) — company.model.ts

Legal/registration data for invoices, footer legal text, compliance pages. Not user-facing branding — this is the registered entity behind the brand.

featureFlags (required) — feature-flags.model.ts

Coarse on/off switches for major product areas (wishlist, blog, chat, loyalty, gift cards, invoices...). Distinct from features below — this set gates bigger surfaces.

features (optional) — features-config.model.ts

Finer-grained per-marketplace toggles (comments, recommendations, badges, etc). Omit any key to fall back to DEFAULT_MARKETPLACE_FEATURES_CONFIG (all true).

apiEndpoints (required) — api-endpoints.model.ts

Per-surface endpoint overrides. bootstrap itself is always required; website/builder/backoffice may stay empty objects to use defaults.

localization (required) — localization.model.ts

Locale list, default, per-locale currency, and dictionary URLs (/assets/i18n/<locale>.json or a CDN URL). Every locale in tenant.supportedLocales needs an entry here.

seo (required) — seo.model.ts

Default meta tags plus per-pageKey overrides, consumed by seo.service.ts.

permissions (required) — permissions.model.ts

Role → permission-key map used by frontend guards. The frontend never hardcodes role logic beyond hiding affordances — see BACKEND-INTEGRATION.md §4.6; the authoritative check still happens server-side per request.

header (optional) — header-config.model.ts

Which header elements show (showSearch, showCart, showRegion, ...) and layout (default | centered). Omit to use DEFAULT_HEADER_CONFIG.

catalog (optional) — catalog-config.model.ts

Product-listing behavior: layout, sort options, enabled filters, which badges/breadcrumbs show.

layout (optional) — layout.model.ts

Top-level page shell type.

navigation (required) — navigation.model.ts

Header and footer link lists, each entry { id, labelKey, route, icon?, order }. labelKey resolves against the locale dictionaries in localization.

Payment-method icons, per-locale copyright text, legal page keys to link.

productPage (optional) — product-page-config.model.ts

Reviews, questions, tabs, related-products behavior on the PDP.

userExperience (optional) — user-experience-config.model.ts

Wishlist, compare, recently-viewed, share, saved-searches — limits and toggles.

pages (required) — page.model.ts

The actual page tree. Each page has a route, layout, and a sections[] list; each section has layout (hero | grid | carousel | ...), responsive visibility, and widgets[]. Each widget references a type + version resolved against the widget manifest (see widgetRegistry) and carries its own props (usually per-locale strings). This is what the page builder edits and what section-engine.service.ts renders.

staticPages (optional) — static-page.model.ts

Simple route → per-locale { title, html } pages (about, privacy, terms, contacts) that don't need the full section/widget builder.

widgetRegistry (optional) — widget-registry.model.ts

URL to the widget manifest — the catalog of widget types/versions this brand's pages[].sections[].widgets[] are allowed to reference. See widget-manifest.service.ts.

modules (optional) — platform-modules.model.ts

Platform-level capability gates that introduce a whole new scope (currently just sellerManagement), not a simple toggle. Absent or undefined = every module disabled, and existing marketplaces that never send this field behave exactly as before (ADR-011). A disabled module must add zero new routes/menus/API calls.

seller (optional, backend-resolved only) — seller.model.ts

Present only when modules.sellerManagement.enabled is true and the request resolves beneath a specific seller. The frontend never decides this itself — same rule as tenant resolution (ADR-001): the backend resolves scope from the verified host/session, never from a client-supplied field.


Required vs optional at a glance

Required Optional (sensible defaults exist)
schemaVersion, generatedAt features
tenant header
branding catalog
theme layout
company footer
featureFlags productPage
apiEndpoints userExperience
localization staticPages
seo widgetRegistry
permissions modules
navigation seller (backend-resolved, never client-set)
pages

Going live — checklist

  • Marketplace row created (§11 of BACKEND-INTEGRATION.md), lifecycleState progressed to production_ready
  • MarketplaceDomain row(s) added, type: 'production'
  • DNS A record → server IP
  • TLS: wildcard subdomain (no action) or add-domain.sh / reconciler for a custom domain
  • api.<base-domain> configured (configure-api-domain.sh) — CORS echoes the exact storefront origin, never * with credentials
  • Backend returns full bootstrap JSON for that Host — validate with curl -fsS https://api.<domain>/bootstrap | jq .
  • curl -I https://<domain>/health200
  • Every locale in tenant.supportedLocales has a localization.dictionaries[] entry and a localization.currencyByLocale entry
  • navigation.header/footer routes match real routes; staticPages/pages[].route keys line up with legalPageKeys