Unreleased documentation. These pages follow the main branch and can change before the next release. For supported guidance, use v0.26.1.
Registry Discovery is a curated index of public Evidence Gateway and Registry Relay advertisements. It resembles a Yahoo-style directory: the catalog operator decides which public descriptions to index, and an application decides whether to trust and directly use a selected provider.
A small role between providers and applications
Section titled “A small role between providers and applications”A provider publishes one closed JSON-LD description containing its public service advertisement. The catalog operator keeps an explicit allowlist of those description URLs and performs a bounded, one-shot build. The build records each origin URL, fetched-byte digest, and fetch time beside every indexed service record.
The profile uses a selected set of Data Catalog Vocabulary (DCAT) 3, DCAT-AP 3.0.1, and BRegDCAT-AP terms. Registry Discovery does not claim full DCAT-AP or BRegDCAT-AP conformance. Its offline tooling transforms only this pinned context and evaluates a selected Shapes Constraint Language (SHACL) subset; the service runtime does not perform RDF or SHACL processing.
What the catalog indexes
Section titled “What the catalog indexes”An index record preserves a service’s public title, description, endpoint URL, roles, jurisdictions, profile identifiers, and product-specific capability identifiers. Evidence Gateway advertisements carry evidence type IDs. Registry Relay advertisements carry semantic class IDs or operation family IDs. A protected-only Relay may intentionally expose neither public capability collection.
The origin fields answer a narrower question than provider trust: which approved URL supplied these bytes for this index revision? They let an application retain provenance during selection. They do not attest that the provider, its endpoint, or its claims are trustworthy.
What the catalog does not do
Section titled “What the catalog does not do”Registry Discovery is neither a provider federation nor an application gateway. It has no provider registration flow, trust store, authorization decision, credentials, request proxy, procedure model, ranking rule, writable catalog, or mutation route. Its fixed service surface is health, readiness, OpenAPI, service search, and evidence-type resolution.
That boundary keeps responsibility in the native products. Evidence Gateway remains the product that answers a fixed requirement and issues its signed, minimum-disclosure assertion. Registry Relay remains the product that exposes its governed protected read surface. Discovery only helps an application find their public advertisements.
What an application does with a result
Section titled “What an application does with a result”For Evidence Gateway, an application resolves a requirement and jurisdiction to Evidence Type alternatives. Each alternative is an AND-list. The application searches and selects one provider for every required type. For Registry Relay, the application searches the public semantic class and operation family as one correlated tuple, then explicitly selects one record. Discovery preserves the complete Evidence resolution context, Relay tuple, catalog revision, and origin provenance.
After selection, the application validates the selection’s closed structure and capability binding, then applies a synchronous local acceptance policy. Structural validation does not authenticate the origin, establish currentness, or trust the selected service. Successful local acceptance creates an ephemeral accepted service that exposes the native endpoint. The application creates credentials and performs native input and output only after that acceptance.
The same client workflow in Rust, Node.js, and Python
Section titled “The same client workflow in Rust, Node.js, and Python”The maintained clients expose the same boundary in each language: resolve an Evidence requirement
when needed, search with exact filters, convert one exact record into a serializable selection,
validate its structure, then apply synchronous adopter-owned acceptance.
Node.js applications take the discovery namespace of @registrystack/client; Python
applications take the discovery namespace of registry-stack-client, which imports as
registry_client; Rust applications take the registry-discovery-client crate.
const { DiscoveryClient, acceptSelection, renewUnchangedSelection, selectEvidenceAlternative, selectEvidenceService, validateSelectionStructure,} = require('@registrystack/client').discovery;
const expectedEvidence = { serviceKind: 'evidence', serviceId: 'urn:example:service:evidence', endpointUrl: 'https://evidence.example/', legalIssuerId: 'urn:example:issuer', technicalProviderId: 'urn:example:provider', jurisdictions: ['urn:example:jurisdiction'], conformsTo: ['urn:example:evidence-profile'], evidenceTypeIds: ['urn:example:evidence-type'], matchedCapability: { kind: 'evidence-type', id: 'urn:example:evidence-type' }, evidenceResolution: { requirementId: 'urn:example:requirement', jurisdiction: 'urn:example:jurisdiction', mappingRevision: `sha256:${'a'.repeat(64)}`, evidenceTypeListId: 'urn:example:evidence-type-list', evidenceTypeIds: ['urn:example:evidence-type'], mappingId: 'urn:example:mapping', mappingAuthorityId: 'urn:example:mapping-authority', },};
function sameOrderedStrings(actual, expected) { return Array.isArray(actual) && actual.length === expected.length && actual.every((value, index) => value === expected[index]);}
function acceptsExpectedEvidence(candidate) { const actualResolution = candidate.evidenceResolution; const expectedResolution = expectedEvidence.evidenceResolution; return candidate.serviceKind === expectedEvidence.serviceKind && candidate.serviceId === expectedEvidence.serviceId && candidate.endpointUrl === expectedEvidence.endpointUrl && candidate.legalIssuerId === expectedEvidence.legalIssuerId && candidate.technicalProviderId === expectedEvidence.technicalProviderId && sameOrderedStrings(candidate.jurisdictions, expectedEvidence.jurisdictions) && sameOrderedStrings(candidate.conformsTo, expectedEvidence.conformsTo) && sameOrderedStrings(candidate.evidenceTypeIds, expectedEvidence.evidenceTypeIds) && candidate.matchedCapability.kind === expectedEvidence.matchedCapability.kind && candidate.matchedCapability.id === expectedEvidence.matchedCapability.id && actualResolution !== undefined && actualResolution.requirementId === expectedResolution.requirementId && actualResolution.jurisdiction === expectedResolution.jurisdiction && actualResolution.mappingRevision === expectedResolution.mappingRevision && actualResolution.evidenceTypeListId === expectedResolution.evidenceTypeListId && sameOrderedStrings(actualResolution.evidenceTypeIds, expectedResolution.evidenceTypeIds) && actualResolution.mappingId === expectedResolution.mappingId && actualResolution.mappingAuthorityId === expectedResolution.mappingAuthorityId;}
function acceptFreshEvidence(previous, fresh) { const checkedFresh = validateSelectionStructure(fresh); const candidate = previous === undefined ? checkedFresh : renewUnchangedSelection(validateSelectionStructure(previous), checkedFresh); return acceptSelection(candidate, acceptsExpectedEvidence);}
const discovery = new DiscoveryClient('https://discovery.example/');const resolved = await discovery.resolveEvidenceTypes({ requirementId: 'urn:example:requirement', jurisdiction: 'urn:example:jurisdiction',});const context = selectEvidenceAlternative(resolved, 'urn:example:evidence-type-list');
for (const evidenceTypeId of context.evidenceTypeIds) { const results = await discovery.searchEvidenceServices({ evidenceTypeId, ...(context.jurisdiction ? { jurisdiction: context.jurisdiction } : {}), }); if (results.items.length !== 1) { throw new Error('select one reviewed provider explicitly'); } const selection = selectEvidenceService(results, { recordId: results.items[0].recordId, evidenceTypeId, resolution: context, }); const accepted = acceptFreshEvidence(undefined, selection);
// Construct the native Evidence client, credentials, and request only now, // using accepted.endpointUrl and adopter-owned native configuration. console.log(accepted.selection.matchedCapability.id, accepted.endpointUrl);}from registry_client.discovery import ( DiscoveryClient, accept_selection, renew_unchanged_selection, select_relay_service, validate_selection_structure,)from registry_client.relay import RelayClient
EXPECTED_RELAY = { "serviceKind": "relay", "serviceId": "urn:example:service:relay", "endpointUrl": "https://relay.example/", "operatorId": "urn:example:operator", "registryAuthorityId": "urn:example:registry-authority", "jurisdictions": ["urn:example:jurisdiction"], "conformsTo": ["urn:example:relay-profile"], "semanticClassIds": ["urn:example:registered-business"], "operationFamilyIds": ["urn:example:consultation-list"], "relayCapabilityMatch": { "semanticClassId": "urn:example:registered-business", "operationFamilyId": "urn:example:consultation-list", },}
def relay_acceptance_subject(candidate): return {key: candidate.get(key) for key in EXPECTED_RELAY}
def accepts_expected_relay(candidate): return relay_acceptance_subject(candidate) == EXPECTED_RELAY
def accept_fresh_relay(previous, fresh): checked_fresh = validate_selection_structure(fresh) candidate = ( checked_fresh if previous is None else renew_unchanged_selection( validate_selection_structure(previous), checked_fresh ) ) return accept_selection(candidate, accepts_expected_relay)
discovery = DiscoveryClient("https://discovery.example/")results = discovery.search_relay_services({ "semanticClassId": "urn:example:registered-business", "operationFamilyId": "urn:example:consultation-list",})if len(results["items"]) != 1: raise RuntimeError("select one reviewed provider explicitly")selection = select_relay_service(results, { "recordId": results["items"][0]["recordId"], "capabilityMatch": { "semanticClassId": "urn:example:registered-business", "operationFamilyId": "urn:example:consultation-list", },})accepted = accept_fresh_relay(None, selection)
# For a protected surface, add native authorization only after acceptance.relay = RelayClient(base_url=accepted.endpoint_url)page = relay.list_records("businesses", page_size=1)if page["kind"] != "complete" or not page["value"]["items"]: raise RuntimeError("Relay returned no records")record = page["value"]["items"][0]print(record["recordIdentifier"], record["domainData"])The application owns the choice because Discovery does not rank results. The structural validator checks the closed response, complete Evidence alternative or Relay tuple, and capability binding. It does not decide whether the selected origin, issuer, operator, endpoint, or capability is trusted or current. The acceptance callback is synchronous, supplied by the application, and receives no credential or native client. A successful callback creates the ephemeral accepted service used by native client configuration. Native definitions and local policy still supply Evidence purpose, audience, issuer and provider identity, configuration revision, selectors, expected outputs, and Relay resource and operation identifiers.
A saved selection is not current trust
Section titled “A saved selection is not current trust”Persist the plain selection when an application needs an offline handoff. The accepted wrapper is ephemeral and is not a persistence format. Loading a selection and passing structural validation proves only that the saved data still has the closed shape and capability binding. The saved data remains inert until the application applies its current local acceptance policy. Neither loading nor local acceptance establishes that the provider still advertises the service.
Currentness requires an online renewal. For Evidence Gateway, the application re-resolves the
requirement and jurisdiction, explicitly chooses an alternative, re-searches every Evidence Type,
and explicitly reselects each provider. For Registry Relay, it re-searches the correlated semantic
class and operation family, then explicitly reselects the provider. Node.js then compares the old
and freshly selected values with renewUnchangedSelection; Python uses
renew_unchanged_selection; Rust uses renew_unchanged_service_selection. The comparison allows
new fetch provenance and a new global catalog revision for an otherwise unchanged service. It
refuses a changed identity, endpoint, role, jurisdiction, profile, capability, origin, mapping, or
resolution context. A refusal requires a new explicit local decision rather than automatic
acceptance. An unchanged renewal still passes through local acceptance before credentials or native
input and output.
Why the split reduces maintenance
Section titled “Why the split reduces maintenance”The provider maintains one public description URL. The catalog operator maintains a small explicit origins file, any evidence-type mappings, and an intentional build-and-restart loop. The application maintains native provider trust where it already belongs. No component must synchronize a central provider registration database, shared credentials, or a proxy policy.