Preview release. These docs are a work in progress. Pages are still being written, links may break, and structure may shift without notice. Treat everything here as a draft and report issues on GitHub.
Consultation flow
Registry Relay is a read-only HTTP consultation gateway that fronts sensitive tabular sources with entity-shaped routes and caller-specific access controls. Storage table identifiers stay private; callers see only declared entities, fields, filters, and aggregates. For the broader scope of what each project owns, see the boundaries and map page.
Request lifecycle
Section titled “Request lifecycle”The lifecycle has one startup phase (Bind) and four per-request phases (Match scope, Query, Audit, Respond).
Startup: Bind
Section titled “Startup: Bind”At startup, Registry Relay loads config.yaml and validates it against metadata.yaml.
Binding maps each logical dataset and entity from the manifest to physical sources,
physical table columns, declared filters, and scopes.
If a dataset in the manifest has no matching config.yaml entry, startup fails with
runtime.binding.dataset_missing and the process exits non-zero.
Bind runs once per process; the per-request path assumes the bound state is valid.
Per request: four phases in sequence
Section titled “Per request: four phases in sequence”1. Match scope. Relay extracts the bearer token or API key from Authorization: Bearer
or X-Api-Key, verifies the credential, and resolves the caller’s principal.
The caller’s scopes are compared against the scope required by the route.
A caller without the correct <dataset_id>:<suffix> scope receives 403 Forbidden before
any data access occurs.
2. Query. Relay translates the request path and query parameters into an internal
query plan over the bound source tables.
Only fields declared in entities[].fields are projected.
Only filter operations declared in entities[].api.allowed_filters are accepted.
Relay builds the query from its own plan; it never forwards raw SQL or arbitrary filter
expressions to the source.
3. Audit. Before returning the response, Relay writes one JSONL record to the
configured audit sink (stdout, file, or syslog).
The record carries the principal_id, request_id (a ULID also returned in the
x-request-id response header), dataset_id, entity_name, scopes_used, query_params
(names and operators, never values), status_code, and duration_ms.
Registry Platform wraps the record with prev_hash and record_hash for tamper evidence.
For the full field list, see the audit record shape in the Relay reference.
4. Respond. Relay returns the entity-shaped response. The response body contains only projected fields for the authenticated caller’s visible datasets and entities. Storage table identifiers never appear in responses.
Entity-shaped routes vs storage tables
Section titled “Entity-shaped routes vs storage tables”Storage tables are an implementation detail. Public routes reference datasets and entity names, never table ids. The four primary route shapes for row access are:
GET /datasets/{dataset_id}/{entity}: paginated list of entity records.GET /datasets/{dataset_id}/{entity}/{id}: single record by identifier.GET /datasets/{dataset_id}/{entity}/{id}/{relationship}: related records for a declared relationship.GET /datasets/{dataset_id}/aggregates/{aggregate_id}: a pre-configured aggregate over entity records.
Metadata routes under /metadata/* follow the same auth path but require a metadata
scope, not a rows or aggregate scope.
The full route inventory is in the Registry Relay reference.
Scope model
Section titled “Scope model”Scopes are independent. A principal can hold any combination of the following suffixes for any dataset:
metadata: read catalog, DCAT, SHACL, policies, and evidence-offering metadata.rows: read entity records, single records, and relationship traversals.aggregate: run pre-configured aggregates over entity records.verify: call the per-entityverifyroute.evidence_verification: call evidence-offering verification endpoints.admin: trigger data reloads on the admin listener.
A scope is always bound to a specific dataset: social_registry:metadata grants metadata
access to the social_registry dataset only.
A key or token holding social_registry:metadata cannot access vital_events:metadata
unless that scope is also assigned.
The following excerpt from config/example.yaml shows three principals with different
scope sets:
auth: mode: api_key api_keys: - id: statistics_office hash_env: STATS_OFFICE_API_KEY_HASH scopes: - social_registry:metadata - social_registry:aggregate - id: program_system hash_env: PROGRAM_SYSTEM_API_KEY_HASH scopes: - social_registry:metadata - social_registry:aggregate - social_registry:rows - id: identity_verification_service hash_env: VERIFICATION_SERVICE_API_KEY_HASH scopes: - social_registry:metadata - social_registry:evidence_verificationThe statistics office can fetch aggregates and catalog metadata but cannot read individual records. The program system can read records and aggregates. The identity verification service can run evidence verification but cannot read rows. Full scope configuration and API-key provisioning are in Authorize callers.
Closed surfaces
Section titled “Closed surfaces”Five constraints a reviewer can verify against the code:
- Read-only in v1. Entity paths expose only
GET. NoPOST,PUT,PATCH, orDELETEroute is registered; provisioning is out of scope. - Closed filter set. Callers cannot submit arbitrary query expressions. Relay accepts
only the operators declared in
entities[].api.allowed_filtersand builds queries from its own plan. - Storage details stay private. Table names, column names, and connection strings never appear in responses or error messages.
- Filter values stay out of audit. Records capture filter parameter names and operators but never the submitted values, so personal identifiers are not logged.
- No token minting. In OIDC mode, Relay is a resource server: it verifies tokens issued by an external identity provider and never mints or refreshes them.