# [Sources](https://rebilder.com/docs/sources)

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

- **Updated:** 2026-08-12
- **Author:** Rebilder
- **Section:** Concepts
- **Description:** The GatewaySources contract: how the gateway reads your source of truth, field by field, and the guarantees that govern what it renders.
- **Publisher:** Rebilder

## 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: `product` → `policies` → `catalog` → `document` → `collection`.** 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.

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `url` | `string` | yes | Canonical product URL; the linked title points here. |
| `title` | `string` | yes | Product title. |
| `brand` | `string` | no | Rendered as a **Brand** fact line. |
| `description` | `string` | no | Rendered verbatim under a Description heading. |
| `price` | `Money` | yes | Current price. Minor units (see Money below). |
| `compareAtPrice` | `Money` | no | Struck through next to the price when present. |
| `availability` | `Availability` | yes | `'in_stock' | 'out_of_stock' | 'preorder' | 'backorder'`, rendered with deterministic labels (`In stock`, …). |
| `variants` | `ProductVariantSource[]` | no | Rendered as a table: id / title / options / price / availability. |
| `shipping` | `ShippingSource` | no | Front-loaded shipping facts. |
| `returns` | `ReturnsSource` | no | Front-loaded returns facts. |
| `images` | `{ url: string; alt?: string }[]` | no | Rendered as markdown links at the bottom. |
| `attributes` | `Record<string, string>` | no | Material, size-chart facts, etc., rendered verbatim under Details. |

## ProductVariantSource, ShippingSource, ReturnsSource

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `ProductVariantSource.id` | `string` | yes | Variant identifier (SKU-like id shown in the table). |
| `ProductVariantSource.title` | `string` | yes | Variant title. |
| `ProductVariantSource.price` | `Money` | yes | Per-variant price. |
| `ProductVariantSource.availability` | `Availability` | yes | Per-variant stock state. |
| `ProductVariantSource.sku` | `string` | no | Explicit SKU when distinct from `id`. |
| `ProductVariantSource.options` | `Record<string, string>` | no | e.g. `{ Color: "Juniper Green" }`. |
| `ShippingSource.summary` | `string` | yes | Merchant-authored shipping summary. Rendered verbatim. |
| `ShippingSource.freeThreshold` | `Money` | no | Free-shipping threshold. |
| `ShippingSource.regions` | `string[]` | no | Ship-to regions. |
| `ShippingSource.etaDays` | `[number, number]` | no | [min, max] delivery estimate in days, from the merchant. |
| `ReturnsSource.summary` | `string` | yes | Merchant-authored returns summary. Rendered verbatim. |
| `ReturnsSource.windowDays` | `number` | no | Return window in days. |
| `ReturnsSource.url` | `string` | no | Link to the full returns policy. |

## PolicySource

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

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `title` | `string` | yes | Policy title, rendered as a linked heading. |
| `url` | `string` | yes | Canonical policy URL. |
| `body` | `string` | yes | Policy text. Rendered verbatim, never summarized or reworded. |

## CatalogItemSource

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

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `url` | `string` | yes | Item URL; the table’s linked title points here. |
| `title` | `string` | yes | Item title. |
| `price` | `Money` | yes | Item price. |
| `availability` | `Availability` | yes | Same 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.

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `url` | `string` | yes | Canonical URL of this page. |
| `title` | `string` | yes | Page title. |
| `kind` | `DocumentKind` | no | Advisory routing label, **never rendered**: `page`, `service`, `location`, `plan`, `article`, `faq`, `profile`, `event`, `listing`, `job`, `course`, or your own string. |
| `summary` | `string` | no | Verbatim, rendered as a blockquote at the top. |
| `updated` | `string` | no | ISO 8601, verbatim. |
| `access` | `'free' | 'registered' | 'metered' | 'subscriber'` | no | Only sections configured for public delivery are included in the response. Keep protected content behind your existing access controls. |
| `facts` | `Fact[]` | no | The front-loaded block, and the reason this type exists. Your order is authoritative. Capped at 60. |
| `actions` | `ActionSource[]` | no | `{ label, url, kind?, note? }`. Capped at 20, never truncated mid-list. |
| `contact` | `ContactSource` | no | `{ phone?, email?, url?, address?: string[] }`, address in your display order. |
| `sections` | `{ heading?, body }[]` | no | Prose. Emitted only when `access` is `free`. Truncatable under the byte cap. |
| `related` | `LinkSource[]` | no | Cross-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.

> **A site with no products wires only these two** A dental practice, a law firm, a council or a magazine wires `document` and `collection` and nothing else. Same `GatewayConfig`, same adapters, same guarantees. A shop with six products and three thousand gift guides wires all five: `product` for the six, `document` for each guide, `collection` for the index. rebilder.com itself runs on `document` and `collection`.

## 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.

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `url` | `string` | yes | Canonical URL of the listing itself, so an agent can resolve which page it is reading. |
| `title` | `string` | no | Defaults to the fixed `Contents` label. |
| `items` | `CollectionItemSource[]` | yes | `{ url, title, summary?, facts? }`. Capped at 500 rendered rows, and the union of item facts becomes the table columns, capped at 12. |
| `updated` | `string` | no | ISO 8601, verbatim. Emitted as `dateModified` in JSON-LD; the markdown renderer does not render it. |
| `language` | `string` | no | BCP 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

| Field | Type | Notes |
| --- | --- | --- |
| `amount` | `number` | **Integer minor units** (cents for USD): `8900` → `$89.00`. Zero-decimal currencies are handled (`¥4,900`). |
| `currency` | `string` | ISO currency code, e.g. `USD`. |

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

## What rendering guarantees

> **Injection-only** Every substantive value in the output (prices, availability, discounts, shipping and returns text, policy bodies, attributes) is injected verbatim from the source object. The renderer never invents, estimates, or rewords a price, stock state, claim, or policy. The only text it adds is structural scaffolding: fixed labels like `**Price:**`, section headings like `Variants`, deterministic enum labels like `In stock`, and a fixed truncation note.

> **Same substance across formats** Markdown output is a format transformation of the same substance as the canonical HTML page. The renderer has no inputs (requester identity, headers, UA) that could even make different-substance output possible; it is pure, deterministic, and fully offline: no LLM, no network, no clock, no locale.

**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.