Registry stack documentation: machine-readable Markdown.
Index of all pages: https://docs.registrystack.org/v/0.38.0/llms.txt
Full corpus: https://docs.registrystack.org/v/0.38.0/llms-full.txt

# Publish and consume a Registry Discovery index

> Package a local Discovery index from Evidence and Relay advertisements, resolve a requirement, and select exact records to hand to native product trust.

import QuickstartMeta from '../../../components/QuickstartMeta.astro';

Registry Discovery helps an application find which Evidence Gateway or Registry Relay service
can answer a question. In this tutorial you play every role: two providers publish a short public
description of their service, an operator packages those descriptions into an index and serves it,
and a consumer asks the index which services fit a requirement and picks one of each kind.

<QuickstartMeta
  outcome="One immutable local package, one resolved requirement, and exact Evidence and Relay selections."
  time="About 15 minutes after the Rust dependencies are available"
  level="Local development with synthetic data"
  prerequisites={['Git', 'Rust 1.95.0 through rustup', 'Python 3', 'curl', 'shasum', 'Local ports 38080 and 38090']}
/>

## Before you start

Clone the repository and work from its root. Every service in this tutorial listens on loopback
only.

```sh test-skip="the runner starts in a copy of this checkout"
git clone --depth 1 --quiet https://github.com/registrystack/registry-stack.git
cd registry-stack
```

Build the two Discovery binaries from the locked workspace and put them on your `PATH`:

```sh test-skip="builds from source; the runner serves the binaries under test"
. scripts/cargo-runtime-library-path.sh
registry_cargo_build "$PWD" --locked --release -p registry-discovery -p registry-discoveryctl
export PATH="$PWD/target/release:$PATH"
```

`registry_cargo_build` runs `cargo build`. On macOS the binaries also load an AWS-LC FIPS library
that stays in Cargo's build directory, so the helper points `DYLD_FALLBACK_LIBRARY_PATH` at it for
this shell. Run every `discoveryctl` and `discovery` command below in this shell; the second shell
this tutorial opens only runs Python and `curl`.

`discoveryctl` is the operator's offline tool: it checks the authoring project and packages the
index. `discovery` is the runtime that serves one verified package.

## Confirm the provider publications

Each provider describes its service in one public JSON-LD file. The checkout carries an example
description for an Evidence Gateway deployment and one for a Registry Relay deployment. Check that
yours are the files this tutorial was written against:

```sh
shasum -a 256 \
  products/discovery/fixtures/descriptions/evidence.jsonld \
  products/discovery/fixtures/descriptions/relay.jsonld
```

```text test-expect
fc96f3a8cb0d82239425ea5712dceca975a5899e5528616648174da661fae905  products/discovery/fixtures/descriptions/evidence.jsonld
5a34fa469803b7c28b3d5e7134a42398e326a2f173aacae9090d29787bc8f4d7  products/discovery/fixtures/descriptions/relay.jsonld
```

The fixture bytes are deterministic examples of the same closed profile the two native products
derive and package. A provider deploys the exact packaged file at its public description URL; the
Discovery runtime does not reconstruct provider metadata.

{/* Evidence: `products/discovery/fixtures/descriptions/evidence.jsonld` and `relay.jsonld` are the
    product-owned deterministic bytes. `crates/registry-evidence/src/discovery.rs`, test
    `provider_discovery_description_preserves_evidence_type_profile_correlation`, and
    `crates/registry-relay-v2/src/artifacts.rs`, test
    `provider_discovery_description_preserves_semantic_class_operation_family_correlation`, bind
    the profile to native provider packaging. */}

## Check the operator project

Copy the tutorial's operator project out of the checkout, so the package you write stays separate
from the source files:

```sh
cp -R products/discovery/tutorial/project discovery-project
```

As the operator, you decide which providers the index includes. `discovery-project/origins.yaml`
lists the URL of each approved description:

```yaml test-excerpt="discovery-project/origins.yaml"
schemaVersion: registry-discovery/origins/v1alpha1
origins:
  - originId: tutorial-evidence
    catalogUrl: http://127.0.0.1:38090/evidence.jsonld
    profile: registry-discovery-v1alpha1
    enabled: true
  - originId: tutorial-relay
    catalogUrl: http://127.0.0.1:38090/relay.jsonld
    profile: registry-discovery-v1alpha1
    enabled: true
```

You also decide which kind of evidence satisfies a requirement an application may have.
`discovery-project/mappings/adult-status.yaml` says that the adult-status requirement is met by
one Evidence type:

```yaml test-excerpt="discovery-project/mappings/adult-status.yaml"
schemaVersion: registry-discovery/evidence-mapping/v1alpha1
mappingId: urn:example:mapping:adult-status
mappingAuthorityId: urn:example:mapping-authority
requirementId: urn:example:requirement:adult-status
jurisdiction: urn:example:jurisdiction
alternatives:
  - evidenceTypeListId: urn:example:evidence-type-list:adult-status
    evidenceTypeIds:
      - urn:example:evidence-type:adult-status
```

Check both files. Nothing serves the two URLs yet, and the check does not need them to.
`--allow-loopback` accepts the `http://127.0.0.1` URLs; without it, `discoveryctl` accepts only
HTTPS origins, which is what a real deployment uses.

```sh
discoveryctl check --project discovery-project --allow-loopback
```

```text test-expect
valid origins=2 mappings=1
```

`discoveryctl check` validates the files without contacting either URL. The package step in the
next section is the only step that fetches the descriptions.

{/* Evidence: `crates/registry-discoveryctl/src/project.rs`, test
    `check_is_offline_and_accepts_an_unreachable_https_origin`, proves the offline check.
    `crates/registry-discoveryctl/tests/build.rs`, test
    `package_fetches_each_origin_once_and_preserves_semantic_revisions`, covers the one-shot fetch. */}

## Package the index

Play the two providers by serving their descriptions at the URLs `origins.yaml` approved. The
server keeps running, so open a second shell at the root of the checkout and start it there:

```sh test-background="http://127.0.0.1:38090/evidence.jsonld"
python3 products/discovery/tutorial/publication_server.py \
  --descriptions products/discovery/fixtures/descriptions
```

Back in the first shell, package the index:

```sh
discoveryctl package \
  --project discovery-project \
  --output discovery-project/package \
  --allow-loopback
```

```text test-expect
packaged packageDigest=sha256:<package-digest> catalogRevision=sha256:b4b7195f36691c245bf49a88a248049ed899c0c41dbf1a87a386571c0dbfba0f mappingRevision=sha256:332004ca3920c498539180946e8f2637e9998ba7e49cd98f31e19d6f818857ac
```

The two revisions are content addressed. The catalog revision is a digest of what the provider
descriptions mean once normalized, and the mapping revision a digest of the operator's
mappings, so the same inputs build the same revisions on any machine. A provider that
reformats its file without changing its meaning keeps the same catalog revision. To detect any
change to the exact bytes a provider published, compare each record's `originContentDigest`,
which appears in the search results below.

The package digest is different: it covers the exact index file through the package's
`SHA256SUMS` file, and the index records when this run fetched each description and when it was
built. Your package digest therefore differs from anyone else's, and from your own next run.

The package step fetched each description once and wrote the complete package to
`discovery-project/package`. In the second shell, stop the publication server with `Ctrl+C`.
Discovery never contacts a provider while it serves, so nothing below needs it.

## Serve the index

`discovery-project/runtime.yaml` names the listener, the package root, and the request and
response limits. It reads the package root from `DISCOVERY_PACKAGE_ROOT`. Discovery accepts only
absolute paths for both files, so the command below builds them from `$PWD`. It names no origin, mapping, or trust setting: the runtime checks the package
against its `SHA256SUMS` at startup, then serves that index and nothing else.

In the first shell, start Discovery:

```sh test-background="http://127.0.0.1:38080/ready"
DISCOVERY_PACKAGE_ROOT="$PWD/discovery-project/package" \
  discovery --runtime-config "$PWD/discovery-project/runtime.yaml"
```

In the second shell, confirm that it is ready:

```sh
curl -s http://127.0.0.1:38080/ready
```

```json test-expect
{"status":"ready"}
```

## Resolve a requirement

Now play the consumer. An application knows what it needs to establish, such as whether a
person is an adult, but not which provider can tell it. Ask Discovery which Evidence types satisfy
the adult-status requirement in the example jurisdiction:

```sh
curl -s \
  -H 'Content-Type: application/json' \
  --data '{"requirementId":"urn:example:requirement:adult-status","jurisdiction":"urn:example:jurisdiction"}' \
  http://127.0.0.1:38080/v1/evidence-types/resolve
```

```json test-expect
{
  "requirementId": "urn:example:requirement:adult-status",
  "jurisdiction": "urn:example:jurisdiction",
  "mappingRevision": "sha256:332004ca3920c498539180946e8f2637e9998ba7e49cd98f31e19d6f818857ac",
  "alternatives": [
    {
      "evidenceTypeListId": "urn:example:evidence-type-list:adult-status",
      "evidenceTypeIds": ["urn:example:evidence-type:adult-status"],
      "mappingId": "urn:example:mapping:adult-status",
      "mappingAuthorityId": "urn:example:mapping-authority"
    }
  ]
}
```

The answer carries the mapping revision the package step printed, so the application can record which
version of your mapping it relied on. When a requirement has several alternatives, the
application picks one, and then needs a provider for every Evidence type listed in it. Here there
is one alternative with one type.

## Search for the providers

Search for Evidence services that assert the resolved type:

```sh
curl -s --get \
  --data-urlencode 'serviceKind=evidence' \
  --data-urlencode 'evidenceType=urn:example:evidence-type:adult-status' \
  http://127.0.0.1:38080/v1/services
```

```json test-expect
{
  "catalogRevision": "sha256:b4b7195f36691c245bf49a88a248049ed899c0c41dbf1a87a386571c0dbfba0f",
  "items": [
    {
      "recordId": "urn:registrystack:discovery:record:sha256:676659c10ce5cc9d353f4fd2816673c7947e612151efbbe1cc4d42372d9be9d5",
      "bindingId": "urn:registrystack:discovery:binding:sha256:2f9ccc4629c7ea2409d986a9c0ee6e3afe6e63326ca7b1be7d9eb8c69082d96c",
      "serviceId": "urn:example:service:evidence",
      "serviceKind": "evidence",
      "title": "Example Evidence",
      "description": "Public signed evidence assertions",
      "endpointUrl": "https://evidence.example.org",
      "publisherId": "urn:example:publisher",
      "jurisdictions": ["https://publications.europa.eu/resource/authority/territory/DEU"],
      "conformsTo": ["https://registrystack.org/evidence/profile/v1"],
      "evidenceTypeIds": ["urn:example:evidence-type:adult-status"],
      "semanticClassIds": [],
      "operationFamilyIds": [],
      "originId": "tutorial-evidence",
      "originUrl": "http://127.0.0.1:38090/evidence.jsonld",
      "originContentDigest": "sha256:fc96f3a8cb0d82239425ea5712dceca975a5899e5528616648174da661fae905",
      "originFetchedAt": "<fetched-at>"
    }
  ]
}
```

Relay services are found by what they hold and what they let you do with it: a semantic class,
such as a person, and an operation family, such as a lookup. Search for Relay services that look
up a person:

```sh
curl -s --get \
  --data-urlencode 'serviceKind=relay' \
  --data-urlencode 'semanticClass=urn:example:class:person' \
  --data-urlencode 'operationFamily=urn:example:operation:lookup' \
  http://127.0.0.1:38080/v1/services
```

```json test-expect
{
  "catalogRevision": "sha256:b4b7195f36691c245bf49a88a248049ed899c0c41dbf1a87a386571c0dbfba0f",
  "items": [
    {
      "recordId": "urn:registrystack:discovery:record:sha256:aa220c11f493c266bc22adf5dc7ca82fb7a83842e6e887dc0e8e5680f4f84244",
      "bindingId": "urn:registrystack:discovery:binding:sha256:cfbc42c572f4b0dfd81d10cd47c149e54451e2f0fa61f79b842b54eff51fb4e6",
      "serviceId": "urn:example:service:relay",
      "serviceKind": "relay",
      "title": "Example Relay",
      "description": "Public registry query metadata",
      "endpointUrl": "https://relay.example.org",
      "operatorId": "urn:example:operator",
      "registryAuthorityId": "urn:example:authority",
      "jurisdictions": ["https://publications.europa.eu/resource/authority/territory/DEU"],
      "conformsTo": ["https://registrystack.org/relay/profile/v2"],
      "evidenceTypeIds": [],
      "semanticClassIds": ["urn:example:class:person"],
      "operationFamilyIds": ["urn:example:operation:lookup"],
      "originId": "tutorial-relay",
      "originUrl": "http://127.0.0.1:38090/relay.jsonld",
      "originContentDigest": "sha256:5a34fa469803b7c28b3d5e7134a42398e326a2f173aacae9090d29787bc8f4d7",
      "originFetchedAt": "<fetched-at>"
    }
  ]
}
```

Both answers carry the `catalogRevision` the package step printed, because both come from the one
index you packaged. Each result names the description it came from, with the same digest you
checked at the start, and `originFetchedAt`, the time your package step fetched it.

## Select and hand off

Each search found one service, so the choice is made. What the application saves is a
selection: the `recordId` and `bindingId` of the result it chose, what it searched for, and the
`catalogRevision` and `mappingRevision` it chose under. That is enough to explain the choice later
and to notice when the index changes. A selection is public metadata only. It is not a credential,
and it does not make the provider trusted.

Calling the provider is a separate step that does not involve Discovery. The application's own
Evidence Gateway or Registry Relay configuration decides whether it trusts the selected service.
If it does, the application calls the service's `endpointUrl` directly with that product's
client: the Evidence client requests and verifies an assertion, and the Relay client finds the
matching operation in Relay's own metadata and reads from it. No credential, request, or response
passes through Discovery. In an application, the Discovery namespace of the
[unified Registry Stack client](../../reference/client-api/#registry-discovery) runs these
searches and saves selections, from Rust, Node.js, or Python.

{/* Tier-C evidence: `crates/registry-discovery-client/src/selection.rs`, test
    `discovery_metadata_has_no_trust_or_native_io_capability`, excludes trust and native I/O from a
    selection. `crates/registry-discovery-client/tests/native_journey.rs`, test
    `complete_evidence_and_relay_journeys_build_select_trust_and_invoke_natively`, proves that a
    mismatched local trust record creates no credentials or provider traffic, then exercises the
    maintained Evidence and Relay clients after accepted local trust. Maintainer reviewed and
    accepted this trust-boundary claim on 2026-08-14. */}

## Assign the maintenance work

| Role | Maintained input | Change trigger | Handoff |
| --- | --- | --- | --- |
| Assertion provider | Packaged Evidence public description | Public Evidence endpoint, profile, evidence type, or issuer role changes | Publish the exact packaged bytes at the approved URL |
| Data publisher | Packaged Relay public description | Public Relay endpoint, profile, semantic class, operation family, or authority role changes | Publish the exact sealed artifact bytes at the approved URL |
| Operator | `origins.yaml`, mapping files, immutable package, and `runtime.yaml` | An approved origin or mapping changes | Run offline `check`, explicit `package`, deploy the complete package, pin its digest when required, then restart Discovery |
| Consumer or verifier | Saved exact selection and native product trust | A catalog revision, mapping revision, origin digest, binding, or local trust decision changes | Re-evaluate local trust, then call the selected native endpoint directly |

The operator does not run a synchronization scheduler or maintain a writable catalog. A failed
package operation does not replace the deployed package. Deploy a complete new package and restart
the process when the change is intentional.

{/* Tier-C evidence: `products/discovery/README.md` defines check, explicit package, deployment, and restart
    as the maintenance loop. `crates/registry-discoveryctl/tests/build.rs`, test
    `failed_origin_fetch_leaves_the_previous_output_untouched`, proves that a failed collection
    leaves the prior output unchanged.
    `crates/registry-discovery/src/startup.rs`, test
    `runtime_is_closed_and_contains_no_origin_mapping_trust_or_fetch_configuration`, defines the
    runtime configuration boundary. `crates/registry-discovery-client/tests/native_journey.rs`, test
    `complete_evidence_and_relay_journeys_build_select_trust_and_invoke_natively`, proves the
    adopter-owned native trust handoff. Maintainer reviewed and accepted this maintenance-boundary
    claim on 2026-08-14. */}

## Clean up the local services

Stop Discovery in the first shell with `Ctrl+C`, then remove the project copy and the package written
into it:

```sh
rm -rf discovery-project
```

The compiled binaries stay under `target/release`, so a second run reuses them.

## What you built

- The provider roles supplied two deterministic public descriptions.
- The operator checked explicit origins and mappings offline, packaged one immutable index, and
  served it with Discovery.
- The consumer resolved one requirement and found one Evidence and one Relay service, both from
  the same catalog revision, ready to save as selections.
- Trusting and calling the chosen services stays with the application and the product clients,
  outside Discovery.

## Troubleshooting

| Symptom | Cause | Fix |
| --- | --- | --- |
| The publication server or Discovery stops at startup | Port `38090` or `38080` is already in use | Stop the process using the port, then start the service again |
| `discoveryctl package` cannot fetch an origin | The publication server is not running in the other shell | Start the publication server, then run the package step again; a failed package step leaves no partial package |
| `discoveryctl package` refuses the output directory | `discovery-project/package` is left from an earlier run, and a package is never replaced in place | Remove `discovery-project/package`, then run the package step again |
| Cargo cannot resolve dependencies | The clean checkout has not acquired the locked Rust dependencies | Restore network access to the Cargo registry or provide a populated Cargo cache |
| A publication digest differs | The provider bytes do not match the tutorial's reviewed fixture | Restore the clean checkout or review the changed publication before indexing it |
| Native handoff is refused | The selected binding does not match the adopter-owned product trust record | Keep the selection inert, correct the local trust decision, and retry the native handoff |

{/* Tier-C evidence: `crates/registry-discovery-client/tests/native_journey.rs`, test
    `complete_evidence_and_relay_journeys_build_select_trust_and_invoke_natively`, proves the trust
    refusal occurs before credential construction and provider traffic.
    `crates/registry-discoveryctl/tests/build.rs`, test
    `failed_origin_fetch_leaves_the_previous_output_untouched`, proves a failed package step
    writes no package, and test `package_refuses_an_existing_output_without_changing_it` proves an
    earlier package is never replaced. Maintainer reviewed and accepted this trust-boundary claim on 2026-08-14. */}

## Next

- [Package and run a Registry Discovery index](../../configure/discovery/) with your approved HTTPS
  origins, deployment directory, and runtime limits.
- [Registry Discovery is an index](../../explanation/discovery-as-an-index/) explains why trust and
  invocation stay in the native products.
- [Request Evidence from an application](../request-evidence-from-an-application/) shows the native
  Evidence relying-party workflow.
- [Publish a governed SQLite registry](../publish-governed-sqlite-registry/) shows the native Relay
  publication and request workflow.