> ## Documentation Index
> Fetch the complete documentation index at: https://voyant.travel/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Catalog

> The cross-vertical sellable-inventory plane: provenance, editorial overlays, search projection, booking-time snapshots, supply models, and the source-adapter contract that lets owned and sourced inventory share one discovery surface.

The catalog module (`@voyant-travel/catalog`) is the plane every sellable thing in Voyant projects into. An operator runs their own tours, resells a wholesaler's packages, books hotels through a bedbank, and pulls cruises from a cruise line, and all of it has to appear as one normalized catalog in admin search, on the storefront, and in the booking flow. Catalog is the contract and the shared infrastructure that makes that possible without forcing every vertical into one table.

It is deliberately **a contract, not a polymorphic root**. There is no shared `catalog_entries` table and no `kind` discriminator. Each vertical module (inventory, cruises, accommodations, charters, extras) keeps its own schema and operational logic, and adopts a small set of cross-cutting catalog primitives: a field-policy contract, a provenance shape, an overlay store, a booking snapshot table, a search index projection, and a drift event stream. Catalog sits **upstream** of the commitment chain. It answers "what is sellable, in what shape, projected from where, with which editorial overrides applied"; the [bookings](/docs/platform/modules) module answers "how a specific sale is committed".

## Key concepts

<ResponseField name="Catalog Item" type="discovery projection">
  A normalized, sellable discovery and booking record used by admin search, the storefront, the composer, and CMS sync, regardless of provenance. It may resolve to a local [Product](/docs/concepts/glossary) or to sourced inventory. See [Catalog Item](/docs/concepts/glossary).
</ResponseField>

<ResponseField name="Catalog Projection" type="interleaved read model">
  The derived read model that interleaves [Operated Inventory](/docs/concepts/glossary) and [Sourced Inventory](/docs/concepts/glossary) into one search-and-sync surface. The same query returns the operator's own tours next to a resold Viking sailing; one resolves to a Product, the other to a sourced row, but both share the same card shape, facets, and merchandising.
</ResponseField>

<ResponseField name="Provenance" type="source pointer">
  The uniform `source_kind` / `source_ref` / `source_freshness` shape carried by every projection. `source_kind: "owned"` for the operator's own inventory; `"voyant-connect"`, `"direct:tui"`, `"bedbank:hotelbeds"`, `"gds:amadeus"`, and similar for sourced inventory. Provenance is also the durable callback handle for post-book operations against the upstream.
</ResponseField>

<ResponseField name="Field policy" type="per-vertical governance">
  The load-bearing schema decision. Every field on every projection, in every vertical, is declared with a policy row carrying twelve attributes: its class (`managed`, `structural`, `merchandisable`, `volatile-indexed`, `volatile-live`), merge rule, drift severity, reindex scope, snapshot mode, queryability, localization, visibility, edit role, override friction, and source freshness. The registry decides what is overrideable, what gets indexed, what is frozen at booking, and what triggers a reindex.
</ResponseField>

<ResponseField name="Editorial overlay" type="non-destructive override">
  An editorial override keyed by `(entity_module, entity_id, field_path, locale, audience, market)`. Marketing can rewrite a sourced product's title, swap its hero image, and add SEO copy without mutating the upstream source. Overlays resolve through a most-specific-to-least-specific fallback chain across the three variant axes.
</ResponseField>

<ResponseField name="Supply model" type="booking mechanic">
  `dynamic` or `scheduled`: the mechanic axis that forks the search and booking experience. Dynamic inventory is composed live for the customer's dates (TUI flight + hotel, bedbanks); it is search-first with a calendar of prices and no local inventory. Scheduled inventory is a seat in a fixed dated departure drawn from a finite allotment (escorted tours, cruises, owned series); it is departures-first. Supply model is explicit and indexed, separate from `category` (`package`, `tour`, `cruise`, `hotel`, and so on).
</ResponseField>

<ResponseField name="Source adapter" type="public extension point">
  The contract any external feed implements to project into the plane: `discover` (emit projections), `liveResolve` (fetch volatile-live values), `reserve` / `cancel` (forward booking writes), optional `getContent` (rich detail content), and optional outbound push methods. Voyant Connect is one adapter among many; a wholesaler, cruise line, or operator can build their own against the same contract.
</ResponseField>

<ResponseField name="Booking snapshot" type="audit truth">
  A frozen view captured at booking commit, one `booking_catalog_snapshot` row per Catalog Item that participates in the booking. It records what the customer saw, what they paid, and the cancellation terms, even if the upstream source mutates or disappears later.
</ResponseField>

## What it owns

Catalog owns the cross-cutting infrastructure, not any vertical's operational truth.

* **The field-policy contract** (`./contract`): the `FieldPolicy` type, the governance enums, and the inheritance loader. Each vertical writes its own policy file.
* **The provenance shape** (`./provenance`) and the durable sourced-entry store that records what is known locally about each sourced row.
* **The overlay store** (`./overlay`): the Drizzle schema and the resolver-merge logic with the full locale x audience x market fallback chain. It accepts writes from the admin UI, external CMS plugins, bulk import, and AI copy pipelines, all governed by the same field policy.
* **The booking snapshot graph** (`./snapshot`): the `booking_catalog_snapshot` table and capture helpers, including structured `pricing_basis` columns finance and refund engines query.
* **The search index projection** (`./indexer`): the engine-agnostic `IndexerAdapter` contract and a native Typesense implementation (the v1 default), plus per-(locale, audience, market) document materialization and the admin-vs-storefront denormalization topology. Browse-time price sort and filter is first-class through indexed price summaries, with an optional two-stage live rerank helper (`./search/rerank`).
* **The drift event stream** (`./drift`): typed events emitted when an upstream field changes materially, severity-graded by the policy registry.
* **The catalog event taxonomy** (`./events`): visibility-filtered payload builders emitted through `@voyant-travel/core/events` and consumed by the existing webhook pipeline for near-real-time cross-deployment freshness.
* **The booking engine** (`./booking-engine`): the vertical-agnostic `quoteEntity` / `bookEntity` / `cancelEntity` lifecycle and the `SourceAdapterRegistry` that dispatches by `source_kind`.

It does **not** own per-vertical operational logic, pricing engines, availability engines, or the commercial decision. Those live in the verticals, in [commerce](/docs/platform/modules/commerce), and in [inventory](/docs/platform/modules/inventory).

## Live availability search

`liveResolve` resolves volatile fields for an already-selected entity. **Live availability search** is the other motion: search an inventory space (destination, dates, pax) across many providers at once and rank what comes back. It is the foundation of [dynamic packaging](/docs/platform/modules/dynamic-packaging).

* **The fan-out** (`fanOutAvailabilitySearch`, `./search/availability-fan-out`) parallelizes `searchAvailability` across the registered source adapters, gates each by vertical and by the `supportsAvailabilitySearch` capability, enforces a per-connection hard timeout (default 5000ms), and merges everything into one price-ranked `AvailabilityCandidate` list. Each source's outcome is reported in `AvailabilityConnectionResult[]`, so one slow or erroring source is flagged, never fatal.
* **Owned inventory is a first-class source.** A handler registered in the `OwnedAvailabilitySearchHandlerRegistry` (`./search/owned-search-handler`) claims one vertical and is merged into the same ranked list as the sourced adapters, not modeled as a fake external provider.
* **The live offer routes** (`./offers/operator-routes`, `createCatalogOffersAdminRoutes`) serve the sourced-package surface: `POST /v1/admin/catalog/package-offers`, `package-search`, `package-detail`, and `cruise-price`. Sourced packages (such as TUI) sync into the index as a priced **summary**; the bookable dated offers are compiled **live per request** by fanning out to Voyant Connect.

## Working with it

A vertical declares its field policy. Every field it wants to project is a row, so the plane knows how to govern it.

```ts theme={null}
import { defineFieldPolicy } from "@voyant-travel/catalog/contract";

export const productCatalogPolicy = defineFieldPolicy([
  {
    path: "title",
    class: "merchandisable",
    merge: "replace",
    drift: "medium",
    reindex: "entry-locale",
    snapshot: "on-book",
    query: "indexed-column",
    localized: true,
    visibility: ["staff", "customer", "partner"],
    editRole: "marketing",
    overrideFriction: "none",
    sourceFreshness: "sync",
  },
  {
    path: "priceFromAmountCents",
    class: "volatile-indexed",
    merge: "source-only",
    drift: "low",
    reindex: "entry",
    snapshot: "never",
    query: "indexed-column",
    localized: false,
    visibility: ["staff", "customer"],
    editRole: "none",
    overrideFriction: "approval",
    sourceFreshness: "sync",
  },
  // ...one row per projected field
]);
```

Catalog's HTTP surface is resolved from the product graph, not mounted by hand. Search is plain JSON and forks on `supplyModel` through the vertical slice; the public route defaults to the `customer` projection and can return a compact storefront-card shape.

```ts theme={null}
// A storefront listing request against a scheduled (departures-first) surface.
await fetch("/v1/public/catalog/search", {
  method: "POST",
  body: JSON.stringify({
    vertical: "products",
    query: "",
    mode: "keyword",
    sort: "departure-asc",
    projection: "storefront-card",
    pagination: { limit: 12 },
    facets: [{ field: "categorySlugs[]" }, { field: "departureMonths[]" }],
  }),
});
```

The booking engine wraps the source adapter so owned and sourced rows quote and book through one lifecycle. Templates register the adapters they want at startup; the engine dispatches by `source_kind`.

```ts theme={null}
import { createSourceAdapterRegistry } from "@voyant-travel/catalog";

const registry = createSourceAdapterRegistry();
registry.register(demoAdapter()); // source.kind: "demo"
registry.register(voyantConnectAdapter({ /* credentials */ })); // "voyant-connect"

// quoteEntity -> bookEntity captures a booking_catalog_snapshot row for both
// owned and sourced inventory, with identical audit shape. A source.kind that
// has no registered adapter fails with NO_ADAPTER_REGISTERED.
```

Sourced rows also gain rich detail content through the optional `getContent` capability: per-vertical, per-locale content caches serve detail pages with stale-while-revalidate refresh, and an operator can curate a `ro-RO` overlay before the upstream even publishes one. Adapters that do not implement `getContent` fall through to a synthesizer that builds the most complete blob it legitimately can from the projection, overlay, and plane metadata.

<Note>
  Volatile-live fields (the exact room-level total with taxes and fees, live inventory count) are never indexed. They are always fetched on demand through the adapter at quote and checkout time. The index holds only `volatile-indexed` price summaries for browse.
</Note>

## Links to other modules

* **[inventory](/docs/platform/modules/inventory)** is the operated-inventory authoring side. It writes its own catalog policy file and projects Products into the plane; catalog never depends on inventory.
* **[commerce](/docs/platform/modules/commerce)** reads the catalog plane: a commercial decision takes a Catalog Item reference and resolves price, availability, and sellability through the matching adapter.
* **bookings** is where the booking engine lands its rows. The engine reuses bookings as the shared parent so an itinerary can mix owned and sourced lines, and stamps the source pointer and snapshot on each.
* **[distribution](/docs/platform/modules/distribution)** is the outbound inverse: when a booking commits on Voyant, distribution pushes it upstream through the same `SourceAdapter` contract, extended with `pushBooking` / `pushAvailability` / `pushContent`.
* **finance** queries the snapshot's structured `pricing_basis` for invoicing and refunds.

## React package

The `@voyant-travel/catalog-react` family provides the UI. `@voyant-travel/catalog-react/booking-engine` backs the quote, draft, hold, and book surfaces, and pairs with `@voyant-travel/bookings-react/journey` for the customer-facing booking journey. Search, card grids, and merchandising surfaces consume the same HTTP routes the headless package mounts.

## Next steps

<CardGroup cols={2}>
  <Card title="Inventory" icon="calendar-days" href="/docs/platform/modules/inventory">
    Operated product authoring, availability rules, slots, and allotments that feed the plane.
  </Card>

  <Card title="Commerce" icon="tag" href="/docs/platform/modules/commerce">
    The commercial decision that turns a Catalog Item into a buyable price.
  </Card>

  <Card title="Distribution" icon="share-nodes" href="/docs/platform/modules/distribution">
    Channels, commission rules, channel push, and reconciliation for outbound resale.
  </Card>

  <Card title="Glossary" icon="book-open" href="/docs/concepts/glossary">
    The shared vocabulary: Product, Catalog Item, Provenance, Channel, and more.
  </Card>
</CardGroup>
