> For the complete documentation index, see [llms.txt](https://docs.nfh.global/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.nfh.global/build/creating-a-network/catalog-publishing-and-discovery.md).

# Catalog Publishing and Discovery

A **Provider Node (PN)** hosts its own signed catalog files and a signed catalog index on infrastructure it already controls, and every **Discovery Service (DS)** that cares about it crawls, verifies, and indexes that data directly — with no shared, centrally-run relay standing between the two. This page covers everything a PN needs to publish a catalog and everything a DS needs to find and crawl one.

For the full protocol specification behind everything on this page, see [NFH-014 — Decentralized Catalog Publishing and Discovery](https://github.com/beckn/protocol-specifications-v2/blob/decentralised-catalog/docs/Catalog_Publishing_and_Discovery.md) and [NFH-003 — The Beckn Protocol Stack](https://github.com/beckn/protocol-specifications-v2/blob/decentralised-catalog/docs/The_Beckn_Protocol_Stack.md).

***

## What a Provider Node needs, before anything else

Publishing is three things you set up once — get these right first, because nothing below this section works without them:

| Prerequisite                                                         | Why it's required                                                                                                                                                                                          | Where to set it up                                                                                                                                             |
| -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **A signing key already registered on the DeDi Registry**            | Every catalog file and index entry you publish is self-signed with it — a Discovery Service has no other way to verify content came from you                                                               | [Onboarding on the NFH fabric](https://github.com/Networks-for-Humanity/fabric-docs/tree/main/build/onboarding/README.md)                                      |
| **Public HTTPS storage for your catalog files and index**            | A crawler can only fetch what it can reach — your own domain, a CDN, an object store, or a tunnel such as ngrok while testing. This is not a Fabric service you sign up for; it's storage you already have | [Quickstart → Step 3](#step-3--serve-the-files-publicly)                                                                                                       |
| **`meta.catalog_index_urls` set on your Registry Subscriber record** | The only pointer a crawler has to find your index at all — this is a catalog-publishing addition on top of basic network onboarding, not part of it                                                        | [Linking your catalog index for discovery](#linking-your-catalog-index-for-discovery), [Quickstart → Step 4](#step-4--point-your-registry-record-at-the-index) |

There is no publish API call anywhere in this list — publishing a catalog is generating and hosting files, not calling a service. The [Quickstart](#quickstart--host-and-sign-your-first-catalog) below walks through all three in order.

***

## The problem

Without a shared, verifiable way to publish catalogs, Fabric networks fragment quickly:

* Every provider publishes through a different mechanism
* Discovery Services have no independent way to verify that what they index actually matches what a provider published
* A provider has no sovereignty over how or where its own catalog data is hosted or served
* Every provider reinvents product definitions independently

The result is duplicated effort, inconsistent search results, and no canonical source of truth.

***

## How catalog publishing works

Catalog publishing is the discipline and tooling a **Provider Node** uses to publish catalog data that any **Discovery Service** can find, verify, and index. It gives the network:

* **Full publisher sovereignty** — a PN hosts and signs its own catalog files on its own storage (an object store, a CDN, a static site); publishing never requires calling a network API
* **Independent verifiability** — every catalog file and every catalog-index entry is self-signed with the PN's Registry-anchored key, so a Discovery Service verifies content came from the PN that claims to publish it, not from an intermediary's copy of it
* **Incremental updates** — a catalog is a signed baseline plus append-only, signed change files, so a Discovery Service re-fetches only what changed since its last crawl
* **Network-wide templates** — a network can publish canonical resource definitions that providers extend, ensuring data consistency across the network
* **Granular visibility** — a catalog entry declares which networks it is relevant to, letting a network-scoped Discovery Service skip catalogs it doesn't need

A Provider Node is the **source of truth** for what it publishes — a Discovery Service never trusts a relayed copy, only the PN's own signature. Consumer applications never call a PN's catalog storage directly — they query a Discovery Service, which keeps its own index current by crawling every PN it cares about.

**High-level architecture.** A PN's publishing surface sits in two layers: the Registry layer (its manifest and Subscriber record, which change rarely) and the catalog layer (its catalog index, baselines, and change files, which change on every publish). Nothing about this surface is centrally operated — a Discovery Service reaches every layer by resolving the PN's Registry identity and then fetching directly from the PN's own storage.

**End-to-end** — from a PN hosting and signing its catalog, through a Discovery Service crawling and verifying it, to a consumer discovering the result:

```mermaid
sequenceDiagram
    participant PN as Provider Node
    participant Store as PN storage
    participant Reg as Registry
    participant DS as Discovery Service
    participant CN as Consumer Node

    Note over PN: One-time, at onboarding
    PN->>PN: Confirm its signing key is already registered on the Registry
    PN->>Store: Host catalog files, change files, and the catalog index
    PN->>Reg: Add catalog_index_urls to its Registry record's meta section, re-signed
    Note over PN,Reg: PN is now discoverable — no publish call was made

    Note over PN: On every content update
    PN->>PN: Create a new catalog baseline or change file, then sign it
    PN->>PN: Re-sign the catalog index entry
    PN->>Store: Publish the updated files and index

    rect rgb(240, 248, 255)
        Note over DS,PN: Discovery crawl (independent, on its own schedule)
        DS->>Reg: Resolve PN's Registry manifest and Subscriber record
        DS->>PN: GET the catalog index, conditionally
        PN-->>DS: Signed catalog index
        DS->>DS: Verify signature, compare entryVersion to stored cursor
        DS->>PN: GET the baseline or change files required
        PN-->>DS: Signed catalog file or change file
        DS->>DS: Verify signature and digest, apply upserts and removals
    end

    rect rgb(255, 240, 255)
        Note over CN,DS: Consumer discovery
        CN->>DS: POST /discover
        DS-->>CN: 200 OK {status: ACK, messageId}
        DS->>CN: POST /on_discover {matching catalogs}
    end
```

**Key points:**

* **Publishing writes files, it does not call an API** — a PN's own storage and its own Registry record are the only things that change
* **Crawling is pull-based and DS-initiated** — a PN never pushes to a Discovery Service, and a Discovery Service never waits on a PN to be reachable to serve `/discover`
* **Verification happens before indexing, every time** — a Discovery Service that cannot verify a signature or a digest discards the content rather than indexing it
* **`on_discover` is asynchronous** — Discovery Service calls the Consumer Node's `bapUri` with matching results, exactly as before

***

## Where you fit

| You are…                                          | Start here                                                                                     |
| ------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| A **Provider Node** publishing catalogs           | [Quickstart](#quickstart--host-and-sign-your-first-catalog) — host and sign your first catalog |
| A **Discovery Service builder** crawling catalogs | [Hosting & Discovery](#hosting--discovery)                                                     |
| A **Network Facilitator** defining templates      | [The catalog data model → Catalog types](#catalog-types)                                       |

***

## Quickstart — host and sign your first catalog

This walkthrough takes you from zero to a signed, crawlable catalog in a few minutes. By the end, you will have hosted a real catalog and its index on storage you control, pointed your Registry record at it, and confirmed the files are exactly what a Discovery Service's crawler will fetch and verify.

Two prerequisites must be in place first — both one-time setup steps for your organisation:

**1. A signing key registered on the DeDi Registry.** Your Ed25519 signing key must already be registered against your participant identity. Every catalog file and every catalog-index entry you publish is self-signed with this key — a Discovery Service resolves your public key from the Registry to verify it. See [Onboarding on the NFH fabric](https://github.com/Networks-for-Humanity/fabric-docs/tree/main/build/onboarding/README.md) if you haven't joined the fabric yet — this is a prerequisite to catalog publishing, not something publishing sets up for you.

**2. Somewhere public to host static files.** Any storage that can serve plain files over HTTPS works — an object store, a CDN, a static site, even GitHub Pages. This is where your catalog files, change files, and catalog index will live. There is no service to call and no account to open with Fabric to publish — you're pointing at storage you already have.

### Step 1 — Prepare a minimal catalog

A catalog is a plain JSON object. Below is a small valid catalog — one provider, one resource, and one offer that references the resource.

```json
{
  "id": "CAT-FRESHMART-HELLO",
  "descriptor": {
    "name": "FreshMart Hello Catalog",
    "shortDesc": "First catalog from FreshMart"
  },
  "provider": {
    "id": "freshmart-blr-001",
    "descriptor": { "name": "FreshMart Koramangala" }
  },
  "resources": [
    {
      "id": "ITEM-BASMATI-1KG",
      "descriptor": { "name": "India Gate Basmati Rice 1KG" },
      "resourceAttributes": {
        "@context": "https://schema.beckn.io/RetailResource/2.1/context.jsonld",
        "@type": "RetailResource",
        "brand": "India Gate"
      }
    }
  ],
  "offers": [
    {
      "id": "OFFER-BASMATI-HELLO",
      "descriptor": { "name": "Basmati 1KG — Launch Offer" },
      "resourceIds": ["ITEM-BASMATI-1KG"],
      "offerAttributes": {
        "@context": "https://schema.beckn.io/RetailOffer/2.1/context.jsonld",
        "@type": "RetailOffer",
        "isActive": true
      }
    }
  ]
}
```

`resourceAttributes[].@context` declares which domain schema the resource follows, and `offers[].resourceIds` links an offer to the resources it applies to — see [The catalog data model](#the-catalog-data-model) below for the full object shape.

### Step 2 — Generate, sign, and host it with `catalog-publisher`

You don't hand-write baselines, change files, digests, or a self-signing index. The **`catalog-publisher`** plugin for [beckn-onix](https://github.com/beckn/beckn-onix) does this for you: hand it your catalog JSON, and it diffs against what you last published, produces the right kind of file (a fresh baseline the first time, an incremental change file after that), signs everything with your registered key, and writes the resulting catalog index.

The fastest way to see this end-to-end is the [starter-kit repository](https://github.com/beckn/starter-kit), which runs `catalog-publisher` on a live ONIX adapter (`onix-bpp`) with a `catalog/publish` trigger:

```bash
curl -X POST http://localhost:8082/catalog/publish \
  -H "Content-Type: application/json" \
  -d '{
    "context": { "action": "catalog/publish" },
    "message": {
      "catalogs": [ { "id": "CAT-FRESHMART-HELLO", "...": "the catalog from Step 1" } ],
      "publishDirectives": [
        { "catalogId": "CAT-FRESHMART-HELLO", "catalogType": "REGULAR" }
      ]
    }
  }'
```

This call is **unsigned and local** — it's a same-operator trigger on your own adapter, not a network API, so there's no Beckn HTTP Signature to attach here. Signing happens downstream, on the catalog files and index the plugin writes. It responds synchronously:

```json
{
  "status": "COMPLETED",
  "results": [
    { "catalogId": "CAT-FRESHMART-HELLO", "status": "ACCEPTED", "version": 1 }
  ]
}
```

Inspect the output directly — no need to exec into any container:

```
generic-devkit/data/beckn/
  index/
    becknCatalogs.index.json          # the catalog index — entryVersion, baseline, changes[], signature
  catalogs/
    CAT-FRESHMART-HELLO.v1.json.gz    # the signed baseline
    CAT-FRESHMART-HELLO.latest.json.gz
```

Publishing the same `catalogId` again with an edited catalog produces a signed **change file** and bumps the index entry's version, instead of a fresh baseline — the plugin decides this for you. See the starter-kit's [Catalog Publishing walkthrough](https://github.com/beckn/starter-kit/blob/main/README.md#catalog-publishing-catalogpublish) for the full request/response shape, and the [`catalog-publisher` plugin README](https://github.com/beckn/beckn-onix/blob/catalog-publisher/pkg/plugin/implementation/catalogpublisher/README.md) for its design.

### Step 3 — Serve the files publicly

A Discovery Service can only crawl what it can fetch over HTTPS. Point `catalogBaseURL` (in your adapter config, or the equivalent setting for whatever storage you use) at wherever the `index/` and `catalogs/` output above is actually being served from — your own domain, a CDN, or a tunnel such as ngrok for local testing. This is the only value that needs to match the domain your files are actually reachable at.

### Step 4 — Point your Registry record at the index

Add the index's public URL to your existing Registry Subscriber record's `meta.catalog_index_urls` (a list of `{url}` entries — a node may host more than one catalog index):

```json
{
  "subscriber_id": "bpp.freshmart.local-retail.net",
  "type": "BPP",
  "url": "https://bpp.freshmart.local-retail.net/beckn/",
  "meta": {
    "catalog_index_urls": [
      { "url": "https://cdn.freshmart.example/beckn/index/becknCatalogs.index.json" }
    ]
  }
}
```

That's the entire "registration" step — there is no separate catalog-specific onboarding call. A crawler has no other way to find your catalog, so this field being missing is the most common reason a published catalog never shows up in `discover` results.

### Step 5 — Test the round trip with starter-kit

If you did Steps 1–4 inside the [starter-kit](https://github.com/beckn/starter-kit) environment (recommended the first time you do this), confirm it actually works end to end rather than trusting the file layout alone:

1. Send a `discover` request from the BAP Postman collection.
2. Confirm your catalog appears in the `on_discover` results.

A crawler runs on its own schedule, not on-demand — so allow for the same crawl-cadence delay you'd see against a real network rather than expecting an instant reflection of what you just published. If it doesn't show up after a reasonable wait, re-check `meta.catalog_index_urls` on your Registry record first — a missing or wrong pointer there, with no immediate error anywhere, is the most common cause. The starter-kit's [Catalog Publishing walkthrough](https://github.com/beckn/starter-kit/blob/main/README.md#catalog-publishing-catalogpublish) and [Troubleshooting](https://github.com/beckn/starter-kit/blob/main/README.md#troubleshooting) sections cover this in more depth.

### What just happened?

You generated a signed catalog baseline and a signed catalog index using `catalog-publisher`, hosted both on storage you control, and pointed your existing Registry record at the index. No publish API was called, no ACK/NACK was exchanged, and no central service now holds a copy of your catalog — every Discovery Service that cares about your network will independently crawl, verify, and index exactly what you published, on its own schedule.

A consumer application can now find your catalog by querying any Discovery Service that crawls your network — without that Discovery Service ever calling you directly, and without you ever calling it.

***

## The catalog data model

This section builds your mental model of what you are publishing and hosting. It walks through the catalog object, its parts, and the rules that govern how a Discovery Service processes it. If you have not yet published a catalog, run the [Quickstart](#quickstart--host-and-sign-your-first-catalog) first — it will make every concept here much more concrete.

### A catalog at a glance

A **catalog** is the top-level payload you publish. It carries the identity of the publishing provider, the resources they offer, and any pricing or commercial terms attached to those resources.

Here is a complete catalog stripped to its essentials:

```json
{
  "id": "CAT-FRESHMART-2026",
  "descriptor": {
    "name": "FreshMart Daily Catalog",
    "shortDesc": "Fresh groceries — Bangalore South"
  },
  "provider": {
    "id": "freshmart-blr-001",
    "descriptor": { "name": "FreshMart Koramangala" }
  },
  "resources": [ ... ],
  "offers":    [ ... ]
}
```

A catalog must contain at least one of `resources` or `offers` — both is common. How the catalog is *processed* (its type, who it's relevant to, whether it's currently active) is declared **separately**, in the catalog's own entry in your **catalog index** — not inside the catalog object itself. See [Publishing your catalog index](#publishing-your-catalog-index) below.

### Resources

A **resource** is a domain-neutral unit of value. It can be a product SKU on a grocery shelf, an EV charging slot, a clinic appointment, a carbon credit, a job role — anything a network wants to make discoverable.

The core fields (`id`, `descriptor`) are universal. Domain-specific properties live inside `resourceAttributes`, declared using a JSON-LD `@context` and `@type`. This pattern lets any domain extend the schema without modifying the core Beckn protocol.

```json
{
  "id": "ITEM-BASMATI-RICE-1KG",
  "descriptor": {
    "name": "India Gate Basmati Rice",
    "shortDesc": "Aged basmati, 1 kg pack"
  },
  "resourceAttributes": {
    "@context": "https://schema.beckn.io/RetailResource/2.1/context.jsonld",
    "@type": "RetailResource",
    "brand": "India Gate",
    "sku": "BSMTI-1KG-IG"
  }
}
```

The `@context` URI tells consumers which schema vocabulary applies. A Discovery Service uses it during domain schema verification (see [Verification](#verification--what-a-discovery-service-checks-before-indexing)).

### Offers

An **offer** captures the commercial terms under which one or more resources can be obtained — price, discount, eligibility, validity. Offers are *separate from* resources and are linked by `resourceIds`, which means the same resource can carry multiple competing offers from different providers.

```json
{
  "id": "OFFER-BASMATI-MAY26",
  "descriptor": { "name": "May Staples Deal" },
  "resourceIds": ["ITEM-BASMATI-RICE-1KG"],
  "validity": {
    "startDate": "2026-05-01T00:00:00Z",
    "endDate":   "2026-05-31T23:59:59Z"
  },
  "offerAttributes": {
    "@context": "https://schema.beckn.io/RetailOffer/2.1/context.jsonld",
    "@type": "RetailOffer",
    "isActive": true
  },
  "considerations": [
    {
      "id": "PRICE-BASMATI",
      "considerationAttributes": {
        "@context": "https://schema.beckn.io/PriceSpecification/2.0/context.jsonld",
        "@type": "PriceSpecification",
        "value": 95,
        "currency": "INR"
      }
    }
  ]
}
```

#### Provider-level offers

An offer with **no `resourceIds`** applies to all resources published by that provider. Use this for blanket promotions, free-delivery thresholds, loyalty programmes, or any commercial term not tied to a specific resource.

```json
{
  "id": "OFFER-FREE-DELIVERY",
  "descriptor": { "name": "Free delivery on orders above ₹500" },
  "offerAttributes": {
    "@context": "https://schema.beckn.io/RetailOffer/2.1/context.jsonld",
    "@type": "RetailOffer",
    "isActive": true
  },
  "considerations": [
    {
      "id": "DELIVERY-WAIVER",
      "considerationAttributes": {
        "@context": "https://schema.beckn.io/PriceSpecification/2.0/context.jsonld",
        "@type": "PriceSpecification",
        "value": 0,
        "currency": "INR",
        "components": [
          { "type": "DELIVERY", "value": 0, "currency": "INR" }
        ]
      }
    }
  ]
}
```

#### Offer-only catalogs

A catalog can contain offers without any resources of its own. This is how a provider publishes pricing overlays or promotions that reference resources published *by another provider*. The catalog must still carry `id`, `descriptor`, and `provider` — only `resources` is omitted.

> **Discoverability:** Offer-only catalogs and provider-level offers are not independently discoverable via resource search. A Discovery Service resolves and attaches these offers to matching real resources at query time. If no matching real resources exist in the Discovery Service, the offers will not appear in search results.

### Provider

The `provider` block identifies who is publishing the catalog and where they operate. The `availableAt` array carries geo-coordinates and address, enabling spatial search on Discovery Services.

| Field         | Required | Description                                                                                                                                                                |
| ------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`          | yes      | Stable provider identifier within the publisher's namespace                                                                                                                |
| `descriptor`  | yes      | `name`, optional `shortDesc`, `longDesc`, `thumbnailImage`                                                                                                                 |
| `availableAt` | no       | Array of `Location` objects — `geo` (GeoJSON Point) + `address` (structured postal address). Resources inherit provider locations; there is no per-resource location field |
| `rating`      | no       | Aggregate rating across the provider's resources                                                                                                                           |

```json
{
  "id": "freshmart-blr-001",
  "descriptor": {
    "name": "FreshMart Koramangala",
    "shortDesc": "Neighbourhood grocery, open 7am–10pm"
  },
  "availableAt": [
    {
      "geo": { "type": "Point", "coordinates": [77.6278, 12.9352] },
      "address": {
        "streetAddress": "42 80-Feet Road, Koramangala 4th Block",
        "addressLocality": "Bengaluru",
        "addressRegion": "Karnataka",
        "postalCode": "560034",
        "addressCountry": "IN"
      }
    }
  ]
}
```

### Catalog types

Every catalog is one of two types, declared via `catalogType` in `publishDirectives`.

#### REGULAR

A commercial catalog published by a **Provider Node**. Resources represent what the provider is currently offering. REGULAR catalogs can contain resources, offers, or both. Most catalogs on a network are REGULAR.

#### MASTER (template)

A **master catalog** is a network-wide template published by a **network facilitator or domain authority**. It defines the canonical specification for a class of product or service — authoritative name, brand identity, schema type, unit of measure, and any other attributes the network wants to standardise across all providers.

**Why templates exist.** Without templates, every provider on a network independently defines the same product — different naming conventions, inconsistent schema types, missing attributes, conflicting brand spellings. A master resource establishes one authoritative definition that all providers on the network build from. When a consumer searches for "India Gate Basmati Rice 1KG", every store's listing refers back to the same canonical resource — ensuring consistent data across the entire network.

> Master catalogs **do not contain offers**. They are pure resource definitions. Pricing, promotions, and commercial terms belong in REGULAR catalogs.

#### Extending a master in a REGULAR catalog

A Provider Node extends a master resource using `resourceDirectives` inside `publishDirectives`. The provider supplies only the fields unique to their offering — local pricing, stock status, location — and inherits everything else from the template.

```json
"resourceDirectives": [
  {
    "resourceId": "MY-LOCAL-BASMATI",
    "extends": { "masterResourceId": "MASTER-INDIA-GATE-BASMATI-1KG" }
  }
]
```

**Template attributes are immutable.** The provider can *add* new fields but cannot change values defined by the template. This is what guarantees consumers see consistent, authoritative data — canonical brand name, schema type, fixed specifications — regardless of which provider's catalog they are looking at. For the full schema of `publishDirectives` and `resourceDirectives`, see [NFH-014 — Decentralized Catalog Publishing and Discovery](https://github.com/beckn/protocol-specifications-v2/blob/decentralised-catalog/docs/Catalog_Publishing_and_Discovery.md).

### Publishing your catalog index

Your **catalog index** is a signed file you host yourself, listing every catalog you offer. Each entry — matched to one of your catalogs by `catalogId` — carries the metadata that used to live in a message-level publish directive: catalog type, which networks it's relevant to, whether it's currently active, and where to fetch its current baseline and change files.

```json
{
  "nodeId": "bpp.freshmart.local-retail.net",
  "next_update": "2026-07-24T09:00:00Z",
  "catalogs": [
    {
      "catalogId": "CAT-FRESHMART-2026",
      "entryVersion": 7,
      "catalogType": "REGULAR",
      "networkIds": ["local-retail.net/grocery-net"],
      "isActive": true,
      "baseline": { "version": 12, "url": "https://cdn.freshmart.example/beckn/CAT-FRESHMART-2026.v12.json", "size": 48213, "digest": "sha256:..." },
      "changes": [
        { "version": 13, "url": "https://cdn.freshmart.example/beckn/changes/CAT-FRESHMART-2026.v13.changes.json", "digest": "sha256:..." }
      ]
    }
  ]
}
```

The `catalog-publisher` tool (see [Publishing tooling](#publishing-tooling--the-catalog-publisher)) generates and re-signs this index for you on every publish — you supply the catalog content, it works out whether that's a fresh baseline or an incremental change file, and keeps the index's digests, `entryVersion`, and signature in sync automatically.

#### Update modes — baseline vs. change files

Where the old model had a per-catalog `updateMode` on a publish request, incremental delivery is now expressed structurally, as two kinds of file the index points a Discovery Service at:

* **A change file** (the equivalent of `MERGE`) — carries only the resources and offers added, updated, or removed since the previous version, keyed by id. A DS applies it as an upsert/removal patch on top of what it already has.
* **A baseline** (the equivalent of `FULL`) — a complete, self-contained snapshot of the catalog's current resources and offers. Published fresh when a PN chooses to compact an accumulated chain of change files, or whenever a DS is too far behind to catch up incrementally.

A DS decides which to fetch based on how far behind its own cursor is — see [Incremental updates](#incremental-updates) for the crawl-side cutover rule.

#### Visibility — `networkIds`

A catalog entry's `networkIds` declares which Fabric networks it's relevant to. A network-scoped Discovery Service uses this to skip catalogs it doesn't need to index, without fetching them first.

```json
{
  "catalogId": "CAT-WHOLESALE-2026",
  "catalogType": "REGULAR",
  "networkIds": ["acme.net/distributor-net", "acme.net/wholesale-net"]
}
```

**When `networkIds` is omitted**, the catalog is relevant to every Discovery Service that crawls your index — this is the catch-all path, and it lets a Provider Node publish without explicit network configuration and still be broadly discoverable across the Fabric. Unlike the old centralized model, `networkIds` is a **relevance filter, not an access gate** — catalog access is uniformly public; any party with a file's URL can fetch it. A network operator's own membership registry, not this field, governs which catalogs a network-scoped DS chooses to trust and surface.

### Ownership

Ownership is not something a central service enforces on your behalf — it's a direct consequence of self-signing. **A catalog belongs to whoever holds the Registry-registered signing key it's signed with.** A Discovery Service verifies every catalog file and index entry against the key registered for your domain; nobody else can produce a signature that verifies against your key, so nobody else can publish an update a DS will accept as coming from you.

**Catalog ownership.** Only you can publish a new version of a `catalogId` you've already published, because only you hold the key it was originally signed with. There is no separate "ownership check" step — a DS simply never indexes content it can't verify against the key on file for your domain.

**Template resource integrity.** Master resources are published and signed by the network facilitator. Any Provider Node can extend a template with `resourceDirectives[].extends`, but the template's defined values are inherited as-is — a DS applies your own fields on top of the master's, never the reverse, so you can add fields but cannot override what the facilitator's own signed file declares.

**Key rotation.** Add your new key to your Registry manifest and start signing new content with it; let already-published content signed by the old key age out naturally rather than eagerly re-signing your entire catalog history. Retiring a key because it was compromised is different — content signed by a key being revoked for compromise must be re-signed or accepted as unverifiable. See [NFH-014 §10.1](https://github.com/beckn/protocol-specifications-v2/blob/decentralised-catalog/docs/Catalog_Publishing_and_Discovery.md#101-onboarding-and-steady-state-publish) for the full rationale.

### Verification — what a Discovery Service checks before indexing

There is no central validation step your publish request waits on. Instead, every Discovery Service that crawls your catalog independently runs the same checks before it will index anything:

1. **Registry manifest and Subscriber record signature** — resolved via the same DeDi lookup every actor already uses.
2. **Catalog index signature** — verified against your Registry-registered key before anything in the index is trusted.
3. **File digest** — every baseline or change file's bytes must match the digest declared in the (now-trusted) index.
4. **File signature** — each catalog file and change file carries its own detached signature over its own content.
5. **Domain schema conformance** — a resource's `resourceAttributes`, an offer's `offerAttributes`, and a consideration's `considerationAttributes` must conform to the domain schema declared in their `@context` URI, exactly as before.

A file that fails any of these checks is discarded — never indexed, never partially applied. Because this verification happens at crawl time, on the DS side, rather than synchronously at your publish time, there's no ACK/NACK round-trip to wait on when you publish: the `catalog-publisher` tool validates structure locally, before you ever write a file to storage, and a Discovery Service's own crawl log is where you'd see a downstream verification failure (see [Error handling](#error-handling)).

### Catalog lifecycle — active, paused, retired

A catalog entry in your index carries `isActive`, which you can toggle freely in either direction — a paused catalog stays fully indexed by every DS that has already crawled it, just excluded from what they treat as currently transactable. Retiring a catalog is one-way: populate the entry's `retiredAt` before you stop publishing updates for it, and never unset it. A DS treats a `retiredAt` entry as no longer offered and stops serving it via `/on_discover` — but it never infers retirement from your catalog simply disappearing from the index, since that could just as easily be a partial crawl on its end. See [NFH-014 §10.4](https://github.com/beckn/protocol-specifications-v2/blob/decentralised-catalog/docs/Catalog_Publishing_and_Discovery.md#104-catalog-entry-lifecycle) for the full state machine.

***

## Hosting & Discovery

A catalog you publish is not pushed anywhere — there is no subscription to register and no push endpoint to implement. Every Discovery Service that cares about your network **crawls, verifies, and indexes your catalog directly**, on its own schedule, by resolving your Registry record and fetching from your own storage. This section explains what a Discovery Service does with what you've hosted, so you can host it correctly and reason about how quickly your updates propagate. For the full crawl and verification specification this section summarizes, see [NFH-014 §10.2 — Discovery crawl](https://github.com/beckn/protocol-specifications-v2/blob/decentralised-catalog/docs/Catalog_Publishing_and_Discovery.md#102-discovery-crawl).

### How a Discovery Service finds your catalog

A crawl pass has three phases: resolve your identity, fetch and verify your index, then fetch and verify only what has changed.

```mermaid
sequenceDiagram
    actor DS as Discovery Service
    participant Reg as Registry
    participant PN as PN storage

    DS->>Reg: Enumerate PNs relevant to a networkId
    DS->>PN: GET the Registry manifest at the well-known path, conditionally
    PN-->>DS: Registry manifest, signed
    DS->>DS: Verify the manifest signature against the Registry-registered key
    DS->>PN: GET the Beckn Subscriber record referenced by the manifest
    PN-->>DS: Subscriber record, including meta.catalog_index_urls, signed
    DS->>DS: Verify the Subscriber record digest against the manifest
    DS->>PN: GET each catalog_index_urls entry, conditionally
    PN-->>DS: Catalog index, self-signing per entry
    loop for each catalog entry
        DS->>DS: Verify the entry signature, compare entryVersion and content-lineage versions to the stored cursor
        alt entry unchanged
            DS->>DS: Skip, nothing to fetch
        else entry changed
            DS->>PN: GET the baseline or change files required by the cutover rule
            PN-->>DS: Catalog file or change file, self-signed
            DS->>DS: Verify the file signature and digest, schema-validate, apply upserts and removals, advance the cursor
        end
    end
    Note over DS: Consumers query /discover against this index, they never wait on a crawl
```

A DS must perform every verification step shown above before indexing any catalog content, and must not index a catalog file or catalog-index entry that fails any of them. A DS should use conditional HTTP requests (`ETag`/`If-Modified-Since`) so an unchanged catalog costs it one cheap poll, not a re-fetch.

**Enumerating PNs.** A crawler either enumerates candidate domains and reads each one's Registry manifest itself, or uses a registry service that caches records with a reverse lookup ("which index URIs are relevant to me"). Either is conformant — a registry service is never treated as an authority, since every file a DS ingests is still verified at its own source.

**How a DS notices a brand-new PN.** There is no publish-time event a PN emits and no subscription a DS registers — a new publisher is simply picked up the next time a DS re-runs its own enumeration pass over the Registry for the `networkId`(s) it cares about. The moment a PN's Registry record carries `meta.catalog_index_urls`, it's a normal candidate on the DS's next pass; there is no separate "new node" fast path. This is why both halves of [Before you're crawlable](#before-youre-crawlable) need to already be true *before* you expect to show up — a DS has no way to notice you sooner than its own enumeration cadence, other than the optional out-of-band change signal in [NFH-014 §10.6](https://github.com/beckn/protocol-specifications-v2/blob/decentralised-catalog/docs/Catalog_Publishing_and_Discovery.md#106-async-trigger-conditions), which still requires the DS to fully verify everything itself before trusting it.

### Before you're crawlable

Before any Discovery Service will find your catalog, two things must be true:

| Requirement                                                                   | What it's for                                                                                                                     |
| ----------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| **Your catalog files and index are hosted at a publicly reachable HTTPS URL** | A crawler can only fetch what it can reach — your own domain, a CDN, an object store, or a tunnel such as ngrok for local testing |
| **`meta.catalog_index_urls` is set on your Registry Subscriber record**       | The only pointer a crawler has to find your index in the first place                                                              |

Neither of these is a call to any Fabric API — see [Quickstart](#quickstart--host-and-sign-your-first-catalog) for hosting your first catalog and registering the pointer.

### Incremental updates

Each catalog entry in your index carries a **baseline** (the latest full snapshot) and a list of **changes** files, one per publish, each holding only the resources and offers added, updated, or removed since the previous version. A crawler remembers the last version it applied:

* **Slightly behind** — fetch only the change files after its cursor.
* **New, or far behind** — fetch the baseline, then the changes after it.
* **Changes too large relative to the baseline** — the *cutover rule*: if the combined size of pending change files exceeds a threshold fraction of the baseline's size, a DS should fetch the baseline instead — it's cheaper.

```mermaid
flowchart LR
  C["Crawler<br/>(remembers last version)"]
  IDX["Index head"]
  SEG["change files<br/>(small)"]
  BASE["baseline<br/>(full catalog)"]

  C -->|"read"| IDX
  IDX -->|"a little behind"| SEG
  IDX -->|"new / far behind /<br/>changes too large"| BASE
```

**Compaction.** When your change-file chain grows long, you may fold everything into a fresh baseline: publish the new full file at a new URL, point the index's `baseline` at it, and keep — not merely host, keep listed — the superseded change files for at least one full `next_update` cycle, so a crawler mid-lineage can still reach the new baseline by applying diffs. A crawler needs no special handling for compaction — its rule is already complete: if the changes it needs are listed, take them; otherwise take the baseline.

The `catalog-publisher` tool computes diffs, digests, and compaction for you (see [Publishing tooling](#publishing-tooling--the-catalog-publisher)) — you supply the current catalog content and it decides what kind of file to write.

### Error handling

A Discovery Service that cannot verify something discards it — it never indexes unverified content, and it never treats an absence as proof of removal.

| What the DS observes                                                                                                                    | What it does                                                                                                                                                                      |
| --------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| A catalog file's digest doesn't match the index entry's declared digest                                                                 | Discards the file; does not index it; logs the failure                                                                                                                            |
| A catalog file's own signature fails verification                                                                                       | Discards; does not index; logs                                                                                                                                                    |
| A catalog-index entry's signature fails verification                                                                                    | Discards the entry and all its files; does not index; logs                                                                                                                        |
| `entryVersion` or a content-lineage version regresses relative to the stored cursor                                                     | Flags as a possible rollback/tamper condition; does not apply the regressed content                                                                                               |
| A previously-indexed `catalogId` is missing from a later, successfully-fetched, validly-signed index, with no `retiredAt` ever observed | Treats this as a possible incomplete crawl, not a removal — logs it, re-verifies next cycle, and does **not** delete the catalog's previously-indexed content on that basis alone |

See [NFH-014 §10.5](https://github.com/beckn/protocol-specifications-v2/blob/decentralised-catalog/docs/Catalog_Publishing_and_Discovery.md#105-error-flows) for the complete error-flow table.

### Master/Regular resolution

A REGULAR catalog's index entry carries `dependencies.masters`, pointing at the MASTER catalog(s) its resources extend:

```json
"dependencies": {
  "masters": [
    { "catalogId": "open-economy.nfh.global/electronics-master", "version": 12, "indexUrl": "https://cdn.open-economy.nfh.global/beckn/catalog-index.json" }
  ]
}
```

A DS inherits attributes from a REGULAR resource's declared MASTER resource, with the REGULAR resource's own fields taking precedence — the same merge semantics as before. `indexUrl` is a hint, not a trust delegation: a DS must verify whatever it fetches via `indexUrl` exactly as it would via ordinary discovery, and must fall back to standard Registry resolution if the hint is stale, unreachable, or fails verification.

***

## Linking your catalog index for discovery

Hosting signed files is not, by itself, enough to be found — a Discovery Service has exactly one way to locate your catalog index, and it's this link. This is deliberately separate from basic fabric onboarding ([Onboarding on the NFH fabric](https://github.com/Networks-for-Humanity/fabric-docs/tree/main/build/onboarding/README.md)), because it's a catalog-publishing addition on top of that base flow, not a step inside it — a consumer node or a provider node that never publishes catalogs never needs it.

**Where it lives.** Your Beckn Subscriber record — the same record you already publish for basic identity and routing — carries a `meta` object alongside its base fields. `catalog_index_urls`, inside `meta`, is a list of `{url}` entries, one per catalog index you host:

```json
{
  "subscriber_id": "bpp.freshmart.local-retail.net",
  "type": "BPP",
  "url": "https://bpp.freshmart.local-retail.net/beckn/",
  "meta": {
    "catalog_index_urls": [
      { "url": "https://cdn.freshmart.example/beckn/index/becknCatalogs.index.json" }
    ]
  }
}
```

**Why a list, not a single URL.** A node can host more than one catalog index — for example, a platform onboarding many providers as one node keeps one catalog index per provider, or a node participating in more than one network segments its catalogs across indexes. Every entry is just another `{url}` a crawler adds to what it resolves for you; nothing about the shape changes whether you host one index or several.

**Why this is easy to miss.** `catalog_index_urls` is not part of the base Subscriber record schema every network participant fills in — it's an addition specific to catalog publishing, living under a generic `meta` object. A provider node can complete ordinary network onboarding, register a signing key, and start transacting entirely correctly, while still never having set this field — and nothing in that flow will tell you it's missing. The only symptom is silent: your catalog never appears in `discover` results, with no error anywhere.

**How to know it's working.** There is no confirmation callback — you're editing a record, not calling an API. Verify it yourself:

1. Re-fetch your own Subscriber record from the Registry and confirm `meta.catalog_index_urls` is present and points at a URL that's actually reachable (`curl` it directly).
2. Confirm that URL serves your current, signed catalog index — not a 404, not a stale copy.
3. Run an actual `discover` request against a Discovery Service that crawls your network and confirm your catalog shows up — see [Quickstart → Step 5](#step-5--test-the-round-trip-with-starter-kit) for a concrete way to do this with starter-kit.

A DS has no separate signal for "this specific field just changed" — it picks up a new or updated `catalog_index_urls` value the same way it picks up everything else about you: on its own next crawl pass. See [How a Discovery Service finds your catalog](#how-a-discovery-service-finds-your-catalog) for exactly how that works.

***

## Publishing tooling — the catalog-publisher

Hand-generating signed baselines, change files, digests, and a self-signing index is not something a publisher should do by hand. **`catalog-publisher`** — a plugin for [beckn-onix](https://github.com/beckn/beckn-onix) — does this for you: it diffs each new catalog submission against what you last published, produces a fresh baseline or an incremental change file as appropriate, computes digests, signs everything with your registered key, and writes the resulting catalog index.

The [starter-kit repository](https://github.com/beckn/starter-kit) runs `catalog-publisher` end-to-end as a working example — a `catalog/publish` trigger on a live ONIX adapter that writes real signed catalog files and an index to disk, served publicly so a crawler can pick them up. It's the fastest way to see the whole shape (files on disk, the index format, `catalogBaseURL`, the Registry's `catalog_index_urls`) before wiring up your own storage. See its [Catalog Publishing walkthrough](https://github.com/beckn/starter-kit/blob/main/README.md#catalog-publishing-catalogpublish) and the [`catalog-publisher` plugin README](https://github.com/beckn/beckn-onix/blob/catalog-publisher/pkg/plugin/implementation/catalogpublisher/README.md) for the full design and configuration reference — an AI agent building a publisher integration should read both before writing any code.

***

## Example use case

A multi-brand EV charging network wants to list all its charging stations nationwide. The network facilitator first publishes a **master (template) catalog** defining canonical charging resource schemas — connector types, power levels, tariff structures — self-hosted and signed like any other catalog.

Each charging operator then publishes a **REGULAR catalog** extending the master templates, adding their specific stations, live availability, and pricing, hosted on their own storage and pointed to from their own Registry record. Every Discovery Service that crawls the network verifies each submission independently and indexes what it can verify.

One such Discovery Service — Fabric DISCOVR — makes the data available for discovery. A mobility app queries DISCOVR for "CCS2 chargers above 50 kW near Bangalore" and gets real-time results from multiple operators in a single response.

***

## The Fabric capability: Cataloguing

Catalog publishing and discovery realises the Fabric **Cataloguing** capability — a canonical surface for publishing offerings (items, prices, providers, locations, terms) to the entire network in one motion. Catalogues live in Fabric; transactions happen off it.

**What it gives you**

* **Publish once, on your own terms** — self-hosted, self-signed, network-wide reach.
* **Provider identity** — every offering tied to a Registry identity.
* **Structured schemas** — typed items, prices, terms, quantities, geos.
* **Versioning** — offerings change without breaking crawlers, via a signed baseline plus incremental change files.
* **Multi-network publishing** — the same offering can appear in multiple networks via policy.

**Where it sits** — a publishing discipline and reference tooling NFH defines and maintains (the `catalog-publisher` plugin, demonstrated in starter-kit) plus the protocol NFH defines (the schemas for offerings, items, providers, catalog files, and the catalog index), and [DISCOVR](/product-documentation/products/discovr.md) as the discovery surface that crawls and serves it.

**Standards alignment:** Beckn Protocol v2 catalogue schemas · JSON-LD · [NFH-014 — Decentralized Catalog Publishing and Discovery](https://github.com/beckn/protocol-specifications-v2/blob/decentralised-catalog/docs/Catalog_Publishing_and_Discovery.md).


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.nfh.global/build/creating-a-network/catalog-publishing-and-discovery.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
