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>
374 lines
18 KiB
Markdown
374 lines
18 KiB
Markdown
# 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`](../src/app/shared/models/config/bootstrap-config.model.ts) and its per-section models in the same folder. Backend contract: [`BACKEND-INTEGRATION.md`](backend/BACKEND-INTEGRATION.md). Deploy/domain/TLS mechanics: [`DEPLOYMENT.md`](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`](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
|
||
|
||
```json
|
||
{
|
||
"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`](../src/app/shared/models/config/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`](../src/app/shared/models/config/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`](../src/app/shared/models/config/theme.model.ts)
|
||
|
||
Full design-token set: color palette, typography, spacing scale, border radii, shadows. Consumed by [`theme-css-vars.mapper.ts`](../src/app/theme/mappers/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`](../src/app/shared/models/config/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`](../src/app/shared/models/config/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`](../src/app/shared/models/config/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`](../src/app/shared/models/config/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`](../src/app/shared/models/config/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`](../src/app/shared/models/config/seo.model.ts)
|
||
|
||
Default meta tags plus per-`pageKey` overrides, consumed by [`seo.service.ts`](../src/app/services/seo.service.ts).
|
||
|
||
### `permissions` (required) — [`permissions.model.ts`](../src/app/shared/models/config/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`](../src/app/shared/models/config/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`](../src/app/shared/models/config/catalog-config.model.ts)
|
||
|
||
Product-listing behavior: layout, sort options, enabled filters, which badges/breadcrumbs show.
|
||
|
||
### `layout` (optional) — [`layout.model.ts`](../src/app/shared/models/config/layout.model.ts)
|
||
|
||
Top-level page shell type.
|
||
|
||
### `navigation` (required) — [`navigation.model.ts`](../src/app/shared/models/config/navigation.model.ts)
|
||
|
||
Header and footer link lists, each entry `{ id, labelKey, route, icon?, order }`. `labelKey` resolves against the locale dictionaries in `localization`.
|
||
|
||
### `footer` (optional) — [`footer-config.model.ts`](../src/app/shared/models/config/footer-config.model.ts)
|
||
|
||
Payment-method icons, per-locale copyright text, legal page keys to link.
|
||
|
||
### `productPage` (optional) — [`product-page-config.model.ts`](../src/app/shared/models/config/product-page-config.model.ts)
|
||
|
||
Reviews, questions, tabs, related-products behavior on the PDP.
|
||
|
||
### `userExperience` (optional) — [`user-experience-config.model.ts`](../src/app/shared/models/config/user-experience-config.model.ts)
|
||
|
||
Wishlist, compare, recently-viewed, share, saved-searches — limits and toggles.
|
||
|
||
### `pages` (required) — [`page.model.ts`](../src/app/shared/models/config/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`](../src/app/dynamic-renderer/section-engine/section-engine.service.ts) renders.
|
||
|
||
### `staticPages` (optional) — [`static-page.model.ts`](../src/app/shared/models/config/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`](../src/app/shared/models/config/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`](../src/app/widgets/registry/widget-manifest.service.ts).
|
||
|
||
### `modules` (optional) — [`platform-modules.model.ts`](../src/app/shared/models/config/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`](../src/app/shared/models/config/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>/health` → `200`
|
||
- [ ] 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`
|