ConceptsManifest & sections

Manifest & sections

A store’s entire look and layout is data, not code. That data is the manifest: brand, theme tokens, catalog metadata, navigation, compliance settings, and an ordered list of sections. The storefront fetches the manifest when it resolves a store, and renders exactly what the manifest describes. Reordering a store, changing its palette, or adding a hero is a manifest edit — never a deploy.

This is the founding rendering decision (ADR-0003): storefronts are served by a single multi-tenant fleet renderer, and customization is data — theme tokens, a typed section registry, per-store CSS custom properties. There is no per-merchant code in the shared renderer, and there is no template language. A store that genuinely outgrows data goes headless (ADR-0008) — the only custom-code path — and reads the same manifest through the public API.

The shape of a manifest

When a store resolves (by tenant or by verified host), it receives a manifest roughly shaped like:

{
  "store":   { "id": "…", "slug": "…", "status": "live" },
  "brand":   { "name": "…", "logo": "…" },
  "domains": { "primary": "…", "aliases": ["…"] },
  "theme":   { /* color / type / layout tokens */ },
  "catalog": { "storeKey": "…" },
  "template": { "sections": [ /* ordered sections */ ], "pages": [ /* policy pages */ ] },
  "comms":   { /* consent + compliance settings */ }
}
  • store.status gates everything: only a live store serves catalog and checkout. The renderer gates public serving on the manifest’s own store.status, so an approved store’s go-live publishes a live version and the store actually serves. A non-live store renders an “unavailable” state, not an error, and an unknown tenant is a real 404 — never an empty manifest.
  • theme carries color, type, and layout tokens.
  • template.sections is the ordered page. template.pages are the store’s policy pages.
  • comms / compliance drives age gates, cookie consent, and the RUO disclaimers where applicable.

Sections — what actually renders

template.sections is an ordered list of { type, props } entries, each a typed section:

"sections": [
  { "type": "announcement", "props": { "text": "Free shipping over $75" } },
  { "type": "header-nav",   "props": { "links": [{ "label": "Shop", "href": "/shop" }] } },
  { "type": "hero",         "props": { "heading": "…", "cta": { "label": "…", "href": "…" } } },
  { "type": "product-grid", "props": { "collection": "featured" } },
  { "type": "coa-block",    "props": { "…": "…" } },
  { "type": "legal-footer", "props": { "…": "…" } }
]

Both the fleet renderer and the merchant site-builder render these from one source — the @dscodotco/storefront-sections package (renderSection / renderSections). What a merchant arranges in the editor is literally the same component, with the same prop validation, that a visitor sees. Adding or reordering a section in the manifest re-lays-out the store with no code change; a section type the renderer does not know is simply not rendered.

Each section type has a prop schema, and those schemas are versioned like events (ADR-0003): additive within a version, a breaking change is a new section version, and old versions serve until a store’s manifest is migrated. A manifest-validation gate runs before any renderer deploy that changes the registry — at hundreds of stores, a breaking section change would otherwise be a live-manifest migration times every store.

Theme tokens

theme is an inert token tree — colors, type, and layout — that the starter maps onto CSS custom properties (for example --color-primary, --color-ink, --font-display). You style components against those variables rather than hard-coded values, so a manifest theme change reflows the whole store. See Theming for the token conventions, the preset matrix, and how a custom frontend consumes them.

Because tokens are inert data (unlike section prop schemas, which are code), a theme change is never a renderer migration — only a manifest edit.

Navigation is authored as a header-nav section whose props.links is the menu ([{ label, href }, …]). A custom frontend reads exactly that rather than re-declaring its own nav:

const { manifest } = await store.site.get();
const nav = manifest.template?.sections?.find((s) => s.type === "header-nav");
const links = nav?.props.links ?? []; // fall back to collections only on honest absence

The site-builder publish flow

Merchants edit the manifest through the portal’s site builder, which never writes the live store directly. The model is immutable versioned manifests with an active pointer:

  • A draft is a new immutable version the active pointer does not reference — editing the draft leaves the live site untouched.
  • Publish moves the active pointer to a version. Publishing an older version is how a rollback works.
  • Preview mints a short-lived signed token over (tenant, version, expiry); the storefront renders that exact draft version behind a non-dismissible “Preview — not published” banner and forced noindex, bypassing host resolution and the ETag cache.

The fail-closed RUO-claims scan runs on the draft write (PUT /v1/my/site/draft, and equally PUT /v1/my/store/manifest), not on publish. The scanner walks every string in the manifest tree: a CRITICAL finding is a hard 422 and no version row is written, and a scanner error is a 503 (“scan did not run”), never a silent pass. The pointer move — POST /v1/my/site/publish — does not re-scan, because every version row already passed the scan on its write. The net effect is the same guarantee: no unscanned version can go live. See the site builder reference for the routes and scopes.

How it maps to what renders

manifest version (draft write --> RUO-claims scan, fail-closed)
  --> publish (pointer move only, no re-scan)
    --> active pointer moves to that version
      --> renderer resolves store, reads active manifest
        --> renderSections(template.sections, ctx) with theme tokens as CSS vars

A custom frontend replaces the last two steps with store.site.get() (GET /v1/tenants/{tenant}/site), which returns the same active manifest — sections, nav, and theme — under the same ETag and the same honest-absence contract (a non-live or unknown tenant is a 404, a repo failure is a 503, never a silent empty store).