Documentation menu

Sources

The GatewaySources contract: how the gateway reads your source of truth, field by field, and the guarantees that govern what it renders.

Last updated

The contract

A source is a lookup from a request URL to your source-of-truth data. Return the data when the URL is yours; return null (or undefined) when it isn’t. Sync or async both work, but resolvers run on the edge hot path (p95 < 50ms compute budget), so back them with your in-memory catalog, a KV cache, or a fast local store, not an origin round-trip.

GatewaySources
interface GatewaySources {            // all optional; missing source => pass through
  // Commerce shaped.
  product?:    SourceResolver<ProductSource>        // a product detail page
  policies?:   SourceResolver<PolicySource[]>       // shipping, returns, warranty
  catalog?:    SourceResolver<CatalogItemSource[]>  // a priced listing

  // Universal: any page on any site, and any index of them.
  document?:   SourceResolver<DocumentSource>       // a guide, article, service, location, FAQ
  collection?: SourceResolver<CollectionSource>     // an index of documents

  // Optional router. Pure and synchronous. When it names a kind, that resolver
  // is the only one called. Return null to use the fixed order below.
  match?: (url: URL) => SourceKind | null
}

type SourceResolver<T> = (url: URL) => T | null | undefined | Promise<T | null | undefined>
type SourceKind = 'product' | 'policies' | 'catalog' | 'document' | 'collection'

Resolution order & error containment

  • The order is fixed: productpoliciescatalogdocumentcollection. The first source returning data wins, so product wins when several would match a URL. The two universal sources sit below the three commerce ones, which is what makes them safe to add: a store that wires site-wide documents keeps its PDPs rendering as PDPs. Narrow the higher-precedence resolver when two would match. There is no separate setting for it.
  • A source that throws is treated as "no match" and resolution continues with the next source. It never breaks your site.
  • If the render itself fails, the request passes through to HTML; the gateway never substitutes a lower-precedence document for the one that matched.
  • An empty policies/catalog array is no match. So is a missing source: configure only the resolvers you have.

ProductSource

One product detail page. Rendering order is fixed and front-loaded: title (linked to the canonical URL) → brand → price (compare-at struck through when present: ~~$120.00~~ $89.00) → availability → shipping → returns → variants table → description → attributes → images as markdown links. Absent optionals render nothing.

FieldTypeRequiredNotes
urlstringyesCanonical product URL; the linked title points here.
titlestringyesProduct title.
brandstringnoRendered as a Brand fact line.
descriptionstringnoRendered verbatim under a Description heading.
priceMoneyyesCurrent price. Minor units (see Money below).
compareAtPriceMoneynoStruck through next to the price when present.
availabilityAvailabilityyes'in_stock' | 'out_of_stock' | 'preorder' | 'backorder', rendered with deterministic labels (In stock, …).
variantsProductVariantSource[]noRendered as a table: id / title / options / price / availability.
shippingShippingSourcenoFront-loaded shipping facts.
returnsReturnsSourcenoFront-loaded returns facts.
images{ url: string; alt?: string }[]noRendered as markdown links at the bottom.
attributesRecord<string, string>noMaterial, size-chart facts, etc., rendered verbatim under Details.

ProductVariantSource, ShippingSource, ReturnsSource

FieldTypeRequiredNotes
ProductVariantSource.idstringyesVariant identifier (SKU-like id shown in the table).
ProductVariantSource.titlestringyesVariant title.
ProductVariantSource.priceMoneyyesPer-variant price.
ProductVariantSource.availabilityAvailabilityyesPer-variant stock state.
ProductVariantSource.skustringnoExplicit SKU when distinct from id.
ProductVariantSource.optionsRecord<string, string>noe.g. { Color: "Juniper Green" }.
ShippingSource.summarystringyesMerchant-authored shipping summary. Rendered verbatim.
ShippingSource.freeThresholdMoneynoFree-shipping threshold.
ShippingSource.regionsstring[]noShip-to regions.
ShippingSource.etaDays[number, number]no[min, max] delivery estimate in days, from the merchant.
ReturnsSource.summarystringyesMerchant-authored returns summary. Rendered verbatim.
ReturnsSource.windowDaysnumbernoReturn window in days.
ReturnsSource.urlstringnoLink to the full returns policy.

PolicySource

Policy documents (shipping, returns, warranty, …), rendered as linked headings + verbatim bodies.

FieldTypeRequiredNotes
titlestringyesPolicy title, rendered as a linked heading.
urlstringyesCanonical policy URL.
bodystringyesPolicy text. Rendered verbatim, never summarized or reworded.

CatalogItemSource

Collection/catalog listings, rendered as a markdown table (linked title, price, availability).

FieldTypeRequiredNotes
urlstringyesItem URL; the table’s linked title points here.
titlestringyesItem title.
priceMoneyyesItem price.
availabilityAvailabilityyesSame enum as products.

DocumentSource

Any page that is not a product, a policy set, or a priced listing. A guide, a service, a location, an article, an FAQ, a plan, a profile, a job posting. Most of a site is usually documents, and a site with no catalog wires this and collection and nothing else.

The shape is the same promise as the commerce types: every field is authoritative data you supply, and the renderer only transforms format. It never invents, estimates, or rewords a value.

FieldTypeRequiredNotes
urlstringyesCanonical URL of this page.
titlestringyesPage title.
kindDocumentKindnoAdvisory routing label, never rendered: page, service, location, plan, article, faq, profile, event, listing, job, course, or your own string.
summarystringnoVerbatim, rendered as a blockquote at the top.
updatedstringnoISO 8601, verbatim.
access'free' | 'registered' | 'metered' | 'subscriber'noDefault free. sections is emitted only when this is free; anything else renders summary, facts, actions and an access notice. Mechanical, so no renderer bug can leak a paywalled body.
factsFact[]noThe front-loaded block, and the reason this type exists. Your order is authoritative. Capped at 60.
actionsActionSource[]no{ label, url, kind?, note? }. Capped at 20, never truncated mid-list.
contactContactSourceno{ phone?, email?, url?, address?: string[] }, address in your display order.
sections{ heading?, body }[]noProse. Emitted only when access is free. Truncatable under the byte cap.
relatedLinkSource[]noCross-links. Last in the tail, and the first thing dropped under the byte cap.

A Fact is { label, value, note? }, and value is a closed union so every rendering is deterministic and locale-free: text, list, number (with an optional unit), boolean (rendered as a fixed Yes/No), money (with an optional maxValue for a range, and period or per), date (ISO 8601, verbatim), url (scheme allowlisted), and hours.

hours renders a markdown table from { weekly, exceptions?, timeZone, note? }. timeZone is required, because hours without a zone are an ambiguous fact. A weekday with intervals: [] is Closed; a weekday absent from weekly renders Not stated, which is a different fact. The renderer never computes "open now": that is the agent’s job, and doing it here would be inventing a value.

CollectionSource

An index of documents. A guides hub, a services list, a locations directory, a blog archive. Where catalog is the priced listing, this is the unpriced one: it renders as a link list, or as a table when the items carry facts.

FieldTypeRequiredNotes
urlstringyesCanonical URL of the listing itself, so an agent can resolve which page it is reading.
titlestringnoDefaults to the fixed Contents label.
itemsCollectionItemSource[]yes{ url, title, summary?, facts? }. Capped at 500 rendered rows, and the union of item facts becomes the table columns, capped at 12.
updatedstringnoISO 8601, verbatim. Emitted as dateModified in JSON-LD; the markdown renderer does not render it.
languagestringnoBCP 47, verbatim, emitted as inLanguage. Malformed values are dropped.

Items carrying no facts render as a link list. Give the items facts and the same listing renders as a table, which is what makes a 3,000-page guide index answerable in one fetch instead of three thousand.

Money

FieldTypeNotes
amountnumberInteger minor units (cents for USD): 8900$89.00. Zero-decimal currencies are handled (¥4,900).
currencystringISO currency code, e.g. USD.

A non-integer amount throws rather than rounds, because silently altering a price is never acceptable.

What rendering guarantees

Size budget: output is capped at maxBytes (default 5120 bytes, UTF-8). Buying facts are front-loaded, and truncation only ever removes content from the bottom (description, attributes, images) on whole lines, appending a fixed truncation note; the front-loaded facts and the variants table are never removed. If the facts alone exceed the budget, they are emitted anyway: facts are never sacrificed to the byte budget.