Released docs. You are viewing the documentation published with v0.20.0. Development docs are available at Latest.
The Registry Relay clients cover the fixed Relay V2 HTTP surface without importing a deployment’s record, selector, filter, artifact, or SDMX schemas. Rust is the canonical implementation. The Python and Node packages are thin bindings that return native plain values and keep HTTP, route, authentication, Problem, trace, and response validation in Rust.
Availability and compatibility
Section titled “Availability and compatibility”Registry Stack is pre-1.0 Beta software. Keep a client on the same Registry Stack release as the Relay deployment unless a release note states a wider compatibility range.
Registry Stack v0.20.0 is the first release with prebuilt Relay clients. The release workflow
attaches three Python wheels and three Node tarballs to the matching GitHub Release. It does not
publish them to PyPI or npm. The Rust crate has publish = false, remains source-only in the
Registry Stack workspace, and is not published to crates.io.
Python requires version 3.10 or later and uses abi3-py310. Node requires version 22.12 or later.
The release assets cover Linux amd64, Linux arm64, and macOS arm64.
Install a client
Section titled “Install a client”Download Python and Node packages from the GitHub Release that matches the Relay deployment. The
Python asset is named
registry_relay_client-<version>-cp310-abi3-<wheel-platform>.whl. Install the local file:
python -m pip install ./registry_relay_client-<version>-cp310-abi3-<wheel-platform>.whlUse linux_x86_64, linux_aarch64, or macosx_11_0_arm64 for <wheel-platform>. The Node asset
is named relay-client-node-v<version>-<platform>.tgz. Install it into an application:
npm install ./relay-client-node-v<version>-<platform>.tgzNode package platforms are linux-amd64-glibc, linux-arm64-glibc, and macos-arm64. Rust
applications take the source crate from the matching release tag because it is not a registry
package:
[dependencies]registry-relay-client = { git = "https://github.com/registrystack/registry-stack", tag = "v<version>" }Construct a client
Section titled “Construct a client”All three clients accept a prefix-bearing base URL. HTTPS is required except for loopback HTTP. The URL cannot contain credentials, a query, a fragment, or ambiguous empty path segments.
use registry_relay_client::{RelayClient, RelayClientConfig};use url::Url;
let config = RelayClientConfig::new( Url::parse("https://relay.example.invalid/institution-a")?,);let client = RelayClient::new(config)?;RelayClientConfig also provides with_token_provider, with_request_timeout,
with_connect_timeout, with_max_response_bytes, with_user_agent, and
with_trusted_root_certificates. Rust methods are asynchronous.
Python
Section titled “Python”from registry_relay_client import RelayClient
client = RelayClient( base_url="https://relay.example.invalid/institution-a", request_timeout_seconds=30, connect_timeout_seconds=10, max_response_bytes=8 * 1024 * 1024,)Python methods are synchronous. They release the global interpreter lock while the private Tokio
runtime waits for network I/O. trusted_root_certificates accepts a PEM bundle as bytes.
const { RelayClient } = require('@registrystack/relay-client');
const client = new RelayClient({ baseUrl: 'https://relay.example.invalid/institution-a', requestTimeoutMilliseconds: 30_000, connectTimeoutMilliseconds: 10_000, maxResponseBytes: 8 * 1024 * 1024,});Node methods return promises. trustedRootCertificates accepts a PEM bundle as a string.
Authentication
Section titled “Authentication”Authentication is optional. Probes and OpenAPI never ask a configured token provider for a token and never send authorization. Other methods attach a bearer when a provider is configured and Relay accepts credentials for that operation.
| Mode | Rust | Python | Node |
|---|---|---|---|
| No bearer | Omit with_token_provider | Omit authorization | Omit authorization |
| Static bearer | Arc::new(StaticToken::new(token)?) passed to with_token_provider | authorization=token | authorization: { static: token } |
| Private-key JWT | PrivateKeyJwt passed as a TokenProvider | authorization={"private_key_jwt": config} | authorization: { privateKeyJwt: config } |
| Custom provider | Implement the public TokenProvider trait | Not exposed | Not exposed |
The built-in private-key-JWT configuration requires a token endpoint, client identifier, and private signing JWK. It also accepts audience, assertion lifetime, refresh margin, token request and connection timeouts, user agent, and a PEM certificate bundle for the token endpoint. Relay and token-endpoint certificate bundles are independent.
Python wraps the configuration as {"private_key_jwt": {...}} and names its required members
token_endpoint, client_id, and client_key. Its optional members are audience,
assertion_lifetime_seconds, refresh_margin_seconds, request_timeout_seconds,
connect_timeout_seconds, user_agent, and byte-valued trusted_root_certificates. Node wraps
the configuration as { privateKeyJwt: {...} }. Its required members are tokenEndpoint,
clientId, and clientKey; optional members are audience, assertionLifetimeSeconds,
refreshMarginSeconds, requestTimeoutMilliseconds, connectTimeoutMilliseconds, userAgent,
and string-valued trustedRootCertificates. Rust constructs PrivateKeyJwtConfig, builds
PrivateKeyJwt, and passes it through with_token_provider.
The built-in token exchange sends only grant_type, client_assertion_type, and
client_assertion. It does not send scope, an RFC 8707 resource, a body client_id, or
deployment-defined form members. Use a pre-acquired short-lived static bearer when an issuer
requires those members. Rust callers can implement a custom TokenProvider instead.
Operation matrix
Section titled “Operation matrix”| Operation | Rust | Python | Node |
|---|---|---|---|
| Process probes | health, ready | health, ready | health, ready |
| Discovery documents | openapi, service_metadata | openapi, service_metadata | openapi, serviceMetadata |
| Resource discovery | resources, continue_resources, resource | resources, continue_resources, resource | resources, continueResources, resource |
| Record list and search | list_records, search_records, continue_collection | list_records, search, continue_list_records, continue_search | listRecords, search, continueListRecords, continueSearch |
| Record read | read_record | read_record | readRecord |
| Governed lookup | lookup_record | lookup | lookup |
| Generated artifact | artifact | artifact | artifact |
| SDMX documents | sdmx_data, sdmx_structure | sdmx_data, sdmx_structure | sdmxData, sdmxStructure |
Every method performs at most one Relay service exchange. A configured private-key-JWT provider can make a separate token-endpoint exchange when it needs to acquire or refresh a bearer. No client follows redirects, reads ambient proxy configuration, retries a Relay exchange, fetches referenced schemas, or advances pagination automatically.
Request arguments
Section titled “Request arguments”The table uses the wire-level fact name. Python spells multiword keyword arguments in snake case,
such as page_size, access_profile, and dimension_at_observation. Node places optional request
facts in an options object and uses camel case, such as pageSize, accessProfile, and
dimensionAtObservation. Rust uses the request types named in the final column.
health and ready take no arguments.
| Operation | Required arguments | Optional arguments | Rust request type |
|---|---|---|---|
| OpenAPI, service metadata | None | ETag | Option<&StrongEtag> |
| Resource list | None | Page size from 1 through 100; ETag | ResourceListRequest |
| Resource detail | Resource identifier | ETag | Resource identifier as &str |
| Resource continuation | Complete resource continuation | ETag | ResourceContinuation |
| Record list | Resource identifier | Page size, record options, filters, ETag | ListRequest |
| Record search | Resource identifier, search identifier, bbox | Page size, record options, ETag | SearchRequest |
| Record or search continuation | Complete matching continuation | ETag | CollectionContinuation |
| Record read | Resource and record identifiers | Record options; ETag | RecordOptions |
| Governed lookup | Resource and lookup identifiers; selectors | Record options; ETag | LookupRequest |
| Artifact | Artifact identifier | ETag | Identifier as &str |
| SDMX data | Agency, resource, three-part version | Key, constraints, offset, limit, dimension at observation, format, ETag | SdmxDataRequest |
| SDMX structure | Kind, agency, resource, three-part version | ETag | SdmxStructureRequest |
RecordOptions contains fields, access profile, and record format. ListRequest adds a positive
page size and a string-to-string filter mapping. SearchRequest::new requires a BoundingBox
ordered as [west, south, east, north] and can add a positive page size. Python passes list
filters to list_records and requires the bbox keyword on search. Node uses ListOptions for
listRecords and SearchOptions, whose bbox member is required, for search.
Input record formats are json, json-ld, geojson, and json-fg in Python. Node also accepts
geo-json-rfc7946 as an alias for geojson. Rust uses RecordFormat::Json, JsonLd,
GeoJsonRfc7946, or JsonFg.
A lookup selector mapping must contain at least one named string, signed integer, or boolean
value. Python supplies it as selectors and Node as the third lookup argument. Rust builds it
with one or more LookupRequest::selector calls.
SDMX data format is json or csv. The optional constraints are a string-to-string component
mapping. Python passes all SDMX data facts as arguments to sdmx_data; Node passes one
SdmxDataRequest object; Rust constructs SdmxDataRequest::new(agency, resource, version) and
uses its builders. SDMX structure kind is dataflow or datastructure in Python,
dataflow, datastructure, or data-structure in Node, and SdmxStructureKind::Dataflow or
DataStructure in Rust. Every SDMX version has the exact x.y.z form.
Every conditional Rust method takes an optional final &StrongEtag. Python exposes etag as the
last optional keyword. Node exposes it as the final optional argument after any options or request
object. Continuation methods accept only the matching complete continuation and optional ETag.
Response outcomes
Section titled “Response outcomes”Process probes return complete responses only. Cacheable operations return one of two outcomes:
- Rust returns
Conditional::Complete(Complete<T>)orConditional::NotModified(NotModified). - Python returns a mapping with
kind: "complete",value,trace_id, and optionaletag, orkind: "not_modified",etag, andtrace_id. - Node returns an object with
kind: 'complete',value,traceId, and optionaletag, orkind: 'notModified',etag, andtraceId.
Deployment-defined records and collections stay dynamic. Python returns nested mappings, lists, and scalars. Node returns plain JSON values. Rust exposes fixed envelopes around a dynamic JSON record body. Fixed service metadata, resource metadata, page, trace, and ETag envelopes remain typed.
Continuations
Section titled “Continuations”Resource discovery returns this complete continuation projection:
{ "cursor": "<opaque-cursor>"}A record-list continuation has this shape:
{ "route": { "kind": "records", "resource": "<resource>" }, "cursor": "<opaque-cursor>", "format": "json", "accessProfile": "<access-profile>"}A search continuation changes route to include kind: "search" and its exact search
identifier. accessProfile is absent when the first request did not select one. format is one of
json, json-ld, geojson-rfc7946, or json-fg.
Pass the complete returned projection unchanged to the matching continuation method. A continuation binds the opaque cursor to its resource or search route, representation format, and optional access profile. It intentionally does not carry first-page fields, filters, bounding box, or page size. The clients reject raw cursor strings, extra members, wrong-route handoff, and attempts to combine a cursor with first-page choices.
Conditional cache contract
Section titled “Conditional cache contract”A complete cacheable response can carry a validated strong ETag. It has exactly one quoted,
lowercase SHA-256 value. Pass it through the method’s ETag argument to send If-None-Match.
A valid 304 Not Modified response has an empty body, echoes the requested ETag, and carries the
same strictly validated trace context as a complete response. Reuse a stored body only when it was
stored with that exact ETag. The clients do not retain bodies or issue conditional requests on
their own.
Raw documents
Section titled “Raw documents”OpenAPI, generated artifacts, SDMX data, and SDMX structures remain raw protocol documents:
| Language | Complete raw response |
|---|---|
| Rust | RawDocument with media_type() and as_bytes() |
| Python | body: bytes and media_type: str |
| Node | body: Buffer and mediaType: string |
The client validates exact OpenAPI and SDMX media types before returning bytes. An artifact can
declare any single syntactically valid media type, which the client preserves. Every raw method
enforces its body bound. The client does not interpret deployment-defined artifacts or SDMX
payloads. SDMX versions use the exact x.y.z form.
Error contract
Section titled “Error contract”Rust returns RelayClientError variants for configuration, invalid request, token, transport,
Relay Problem, and protocol failures. Python throws RelayClientError with snake-case
attributes. Node throws the same named class with camel-case attributes.
| Meaning | Python | Node |
|---|---|---|
| Closed failure category | kind | kind |
| Registered Problem code | code | code |
| Public HTTP status | status | status |
| Validated trace identifier | trace_id | traceId |
Bounded 429 delay | retry_after_seconds | retryAfterSeconds |
| Transport subcategory | transport_kind | transportKind |
| Token subcategory | token_kind | tokenKind |
Optional error attributes are absent or None when the failure does not carry that fact. A Relay
Problem is accepted only when its exact six-member document, registered code and status, response
media type, and header/body trace agree. Only a registered 429 Problem can expose a numeric
Retry-After from 1 through 60 seconds.
Errors retain fixed local reasons, public status and Problem codes, validated trace identifiers, and bounded retry guidance. They do not expose credentials, JWKs, selectors, filters, response bodies, header values, URLs, or underlying HTTP error chains.
Shared implementation boundary
Section titled “Shared implementation boundary”The canonical Rust client owns product-neutral outbound policy, bearer acquisition, prefix-safe route construction, bounded reads, OAuth response decoding, trace validation, and Problem validation. Relay-specific request models and fixed route semantics stay in the Relay client. Deployment-defined data stays dynamic at the SDK boundary.
The Python and Node bindings convert native values, construct the Rust client, run one SDK method, and map the validated result or error. They do not implement Python or JavaScript HTTP, route, authentication, Problem, redirect, retry, cache, or pagination policy.
Related reference
Section titled “Related reference”- Relayctl command reference for project authoring and package commands.
- Instance API references for the deployment-generated Relay OpenAPI boundary.
- Errors and status codes for the wider Registry Stack error vocabulary.