Skip to content
Registry StackDocsv0.38.0

Publish and consume a Registry Discovery index

For the assertion provider and data publisher and operator and consumer or verifier

View as Markdown

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.

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
GitRust 1.95.0 through rustupPython 3curlshasumLocal ports 38080 and 38090

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

Terminal window
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:

Terminal window
. 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.

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:

Terminal window
shasum -a 256 \
products/discovery/fixtures/descriptions/evidence.jsonld \
products/discovery/fixtures/descriptions/relay.jsonld
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.

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

Terminal window
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:

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:

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.

Terminal window
discoveryctl check --project discovery-project --allow-loopback
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.

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:

Terminal window
python3 products/discovery/tutorial/publication_server.py \
--descriptions products/discovery/fixtures/descriptions

Back in the first shell, package the index:

Terminal window
discoveryctl package \
--project discovery-project \
--output discovery-project/package \
--allow-loopback
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.

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:

Terminal window
DISCOVERY_PACKAGE_ROOT="$PWD/discovery-project/package" \
discovery --runtime-config "$PWD/discovery-project/runtime.yaml"

In the second shell, confirm that it is ready:

Terminal window
curl -s http://127.0.0.1:38080/ready
{"status":"ready"}

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:

Terminal window
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
{
"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 Evidence services that assert the resolved type:

Terminal window
curl -s --get \
--data-urlencode 'serviceKind=evidence' \
--data-urlencode 'evidenceType=urn:example:evidence-type:adult-status' \
http://127.0.0.1:38080/v1/services
{
"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:

Terminal window
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
{
"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.

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 runs these searches and saves selections, from Rust, Node.js, or Python.

RoleMaintained inputChange triggerHandoff
Assertion providerPackaged Evidence public descriptionPublic Evidence endpoint, profile, evidence type, or issuer role changesPublish the exact packaged bytes at the approved URL
Data publisherPackaged Relay public descriptionPublic Relay endpoint, profile, semantic class, operation family, or authority role changesPublish the exact sealed artifact bytes at the approved URL
Operatororigins.yaml, mapping files, immutable package, and runtime.yamlAn approved origin or mapping changesRun offline check, explicit package, deploy the complete package, pin its digest when required, then restart Discovery
Consumer or verifierSaved exact selection and native product trustA catalog revision, mapping revision, origin digest, binding, or local trust decision changesRe-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.

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

Terminal window
rm -rf discovery-project

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

  • 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.
SymptomCauseFix
The publication server or Discovery stops at startupPort 38090 or 38080 is already in useStop the process using the port, then start the service again
discoveryctl package cannot fetch an originThe publication server is not running in the other shellStart the publication server, then run the package step again; a failed package step leaves no partial package
discoveryctl package refuses the output directorydiscovery-project/package is left from an earlier run, and a package is never replaced in placeRemove discovery-project/package, then run the package step again
Cargo cannot resolve dependenciesThe clean checkout has not acquired the locked Rust dependenciesRestore network access to the Cargo registry or provide a populated Cargo cache
A publication digest differsThe provider bytes do not match the tutorial’s reviewed fixtureRestore the clean checkout or review the changed publication before indexing it
Native handoff is refusedThe selected binding does not match the adopter-owned product trust recordKeep the selection inert, correct the local trust decision, and retry the native handoff