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

# Configuration

> The following canonical paths are derived from the complete deserialization schema.

The following canonical paths are derived from the complete deserialization schema.
Named properties use dot notation, `[]` denotes array items, and `.*` denotes map values.
This inventory is checked in both directions so neither the schema nor this reference can gain a key
without the other.

{/* registry-relay-config-key-paths:start */}
```text
audit
audit.chain
audit.format
audit.hash_secret_env
audit.include_health
audit.path
audit.rotate
audit.rotate.max_files
audit.rotate.max_size_mb
audit.sink
audit.write_policy
auth
auth.api_keys
auth.api_keys[]
auth.api_keys[].fingerprint
auth.api_keys[].fingerprint.name
auth.api_keys[].fingerprint.path
auth.api_keys[].fingerprint.provider
auth.api_keys[].id
auth.api_keys[].scopes
auth.api_keys[].scopes[]
auth.failure_throttle
auth.failure_throttle.enabled
auth.failure_throttle.max_failures
auth.failure_throttle.window_seconds
auth.mode
auth.oidc
auth.oidc.allow_dev_insecure_fetch_urls
auth.oidc.allowed_algorithms
auth.oidc.allowed_algorithms[]
auth.oidc.allowed_clients
auth.oidc.allowed_clients[]
auth.oidc.allowed_token_types
auth.oidc.allowed_token_types[]
auth.oidc.audiences
auth.oidc.audiences[]
auth.oidc.discovery_url
auth.oidc.issuer
auth.oidc.jwks_cache_ttl
auth.oidc.jwks_url
auth.oidc.leeway
auth.oidc.scope_claim
auth.oidc.scope_map
auth.oidc.scope_map.*
auth.oidc.scope_object_required_keys
auth.oidc.scope_object_required_keys[]
catalog
catalog.authority_type
catalog.base_url
catalog.default_spatial_coverage
catalog.participant_id
catalog.publisher
catalog.publisher_iri
catalog.title
config_trust
config_trust.antirollback_state_path
config_trust.break_glass_override_path
config_trust.bundle_path
config_trust.trust_anchor_path
consultation
consultation.artifacts
consultation.artifacts.evidence
consultation.artifacts.evidence[]
consultation.artifacts.evidence[].class
consultation.artifacts.evidence[].path
consultation.artifacts.evidence[].sha256
consultation.artifacts.integration_packs
consultation.artifacts.integration_packs[]
consultation.artifacts.integration_packs[].hash
consultation.artifacts.integration_packs[].path
consultation.artifacts.integration_packs[].sha256
consultation.artifacts.private_bindings
consultation.artifacts.private_bindings[]
consultation.artifacts.private_bindings[].hash
consultation.artifacts.private_bindings[].path
consultation.artifacts.private_bindings[].sha256
consultation.artifacts.public_contracts
consultation.artifacts.public_contracts[]
consultation.artifacts.public_contracts[].hash
consultation.artifacts.public_contracts[].path
consultation.artifacts.public_contracts[].sha256
consultation.artifacts.rhai_scripts
consultation.artifacts.rhai_scripts[]
consultation.artifacts.rhai_scripts[].path
consultation.artifacts.rhai_scripts[].sha256
consultation.audit_pseudonym_materials
consultation.audit_pseudonym_materials[]
consultation.audit_pseudonym_materials[].key_id
consultation.audit_pseudonym_materials[].source
consultation.audit_pseudonym_materials[].source.name
consultation.audit_pseudonym_materials[].source.provider
consultation.authorized_workload
consultation.authorized_workload.audience
consultation.authorized_workload.client_claim_selector
consultation.authorized_workload.client_value
consultation.authorized_workload.principal_id
consultation.source_credentials
consultation.source_credentials[]
consultation.source_credentials[].client_id_env
consultation.source_credentials[].client_secret_env
consultation.source_credentials[].generation
consultation.source_credentials[].password_env
consultation.source_credentials[].ref
consultation.source_credentials[].token_env
consultation.source_credentials[].type
consultation.source_credentials[].username_env
consultation.source_credentials[].value_env
consultation.state_plane
consultation.state_plane.audit_pseudonym_keyring_lock_key
consultation.state_plane.chain_key_epoch_id
consultation.state_plane.database_url_env
consultation.state_plane.root_certificate_path
consultation.state_plane.serving_fence_lock_key
datasets
datasets[]
datasets[].access_rights
datasets[].aggregates
datasets[].aggregates[]
datasets[].aggregates[].access
datasets[].aggregates[].access.aggregate_only_execution
datasets[].aggregates[].access.aggregate_scope
datasets[].aggregates[].access.metadata_scope
datasets[].aggregates[].allowed_filters
datasets[].aggregates[].allowed_filters[]
datasets[].aggregates[].allowed_filters[].field
datasets[].aggregates[].allowed_filters[].ops
datasets[].aggregates[].allowed_filters[].ops[]
datasets[].aggregates[].default_group_by
datasets[].aggregates[].default_group_by[]
datasets[].aggregates[].description
datasets[].aggregates[].dimensions
datasets[].aggregates[].dimensions[]
datasets[].aggregates[].dimensions[].codelist
datasets[].aggregates[].dimensions[].field
datasets[].aggregates[].dimensions[].id
datasets[].aggregates[].dimensions[].label
datasets[].aggregates[].disclosure_control
datasets[].aggregates[].disclosure_control.method
datasets[].aggregates[].disclosure_control.method[]
datasets[].aggregates[].disclosure_control.min_cell_size
datasets[].aggregates[].disclosure_control.min_group_size
datasets[].aggregates[].disclosure_control.report_suppressed_rows
datasets[].aggregates[].disclosure_control.suppression
datasets[].aggregates[].group_by
datasets[].aggregates[].group_by[]
datasets[].aggregates[].id
datasets[].aggregates[].indicators
datasets[].aggregates[].indicators[]
datasets[].aggregates[].indicators[].column
datasets[].aggregates[].indicators[].decimals
datasets[].aggregates[].indicators[].definition_uri
datasets[].aggregates[].indicators[].frequency
datasets[].aggregates[].indicators[].function
datasets[].aggregates[].indicators[].id
datasets[].aggregates[].indicators[].label
datasets[].aggregates[].indicators[].unit_measure
datasets[].aggregates[].indicators[].unit_mult
datasets[].aggregates[].joins
datasets[].aggregates[].joins[]
datasets[].aggregates[].joins[].relationship
datasets[].aggregates[].measures
datasets[].aggregates[].measures[]
datasets[].aggregates[].measures[].column
datasets[].aggregates[].measures[].function
datasets[].aggregates[].measures[].name
datasets[].aggregates[].required_filter_bindings
datasets[].aggregates[].required_filter_bindings[]
datasets[].aggregates[].required_filter_bindings[].field
datasets[].aggregates[].required_filter_bindings[].source
datasets[].aggregates[].required_filters
datasets[].aggregates[].required_filters[]
datasets[].aggregates[].source_entity
datasets[].aggregates[].spatial
datasets[].aggregates[].spatial.bbox_fields
datasets[].aggregates[].spatial.bbox_fields.max_x
datasets[].aggregates[].spatial.bbox_fields.max_y
datasets[].aggregates[].spatial.bbox_fields.min_x
datasets[].aggregates[].spatial.bbox_fields.min_y
datasets[].aggregates[].spatial.collection_id
datasets[].aggregates[].spatial.dimension
datasets[].aggregates[].spatial.geometry_entity
datasets[].aggregates[].spatial.geometry_field
datasets[].aggregates[].spatial.geometry_id_field
datasets[].aggregates[].spatial.max_geometry_vertices
datasets[].aggregates[].spatial.mode
datasets[].aggregates[].temporal_field
datasets[].aggregates[].title
datasets[].applicable_legislation
datasets[].applicable_legislation[]
datasets[].conforms_to
datasets[].conforms_to[]
datasets[].defaults
datasets[].defaults.materialization
datasets[].defaults.refresh
datasets[].defaults.refresh.interval
datasets[].defaults.refresh.mode
datasets[].description
datasets[].entities
datasets[].entities[]
datasets[].entities[].access
datasets[].entities[].access.aggregate_scope
datasets[].entities[].access.evidence_verification_scope
datasets[].entities[].access.metadata_scope
datasets[].entities[].access.read_scope
datasets[].entities[].aggregates
datasets[].entities[].aggregates[]
datasets[].entities[].aggregates[].access
datasets[].entities[].aggregates[].access.aggregate_only_execution
datasets[].entities[].aggregates[].access.aggregate_scope
datasets[].entities[].aggregates[].access.metadata_scope
datasets[].entities[].aggregates[].allowed_filters
datasets[].entities[].aggregates[].allowed_filters[]
datasets[].entities[].aggregates[].allowed_filters[].field
datasets[].entities[].aggregates[].allowed_filters[].ops
datasets[].entities[].aggregates[].allowed_filters[].ops[]
datasets[].entities[].aggregates[].default_group_by
datasets[].entities[].aggregates[].default_group_by[]
datasets[].entities[].aggregates[].description
datasets[].entities[].aggregates[].dimensions
datasets[].entities[].aggregates[].dimensions[]
datasets[].entities[].aggregates[].dimensions[].codelist
datasets[].entities[].aggregates[].dimensions[].field
datasets[].entities[].aggregates[].dimensions[].id
datasets[].entities[].aggregates[].dimensions[].label
datasets[].entities[].aggregates[].disclosure_control
datasets[].entities[].aggregates[].disclosure_control.method
datasets[].entities[].aggregates[].disclosure_control.method[]
datasets[].entities[].aggregates[].disclosure_control.min_cell_size
datasets[].entities[].aggregates[].disclosure_control.min_group_size
datasets[].entities[].aggregates[].disclosure_control.report_suppressed_rows
datasets[].entities[].aggregates[].disclosure_control.suppression
datasets[].entities[].aggregates[].group_by
datasets[].entities[].aggregates[].group_by[]
datasets[].entities[].aggregates[].id
datasets[].entities[].aggregates[].indicators
datasets[].entities[].aggregates[].indicators[]
datasets[].entities[].aggregates[].indicators[].column
datasets[].entities[].aggregates[].indicators[].decimals
datasets[].entities[].aggregates[].indicators[].definition_uri
datasets[].entities[].aggregates[].indicators[].frequency
datasets[].entities[].aggregates[].indicators[].function
datasets[].entities[].aggregates[].indicators[].id
datasets[].entities[].aggregates[].indicators[].label
datasets[].entities[].aggregates[].indicators[].unit_measure
datasets[].entities[].aggregates[].indicators[].unit_mult
datasets[].entities[].aggregates[].joins
datasets[].entities[].aggregates[].joins[]
datasets[].entities[].aggregates[].joins[].relationship
datasets[].entities[].aggregates[].measures
datasets[].entities[].aggregates[].measures[]
datasets[].entities[].aggregates[].measures[].column
datasets[].entities[].aggregates[].measures[].function
datasets[].entities[].aggregates[].measures[].name
datasets[].entities[].aggregates[].required_filter_bindings
datasets[].entities[].aggregates[].required_filter_bindings[]
datasets[].entities[].aggregates[].required_filter_bindings[].field
datasets[].entities[].aggregates[].required_filter_bindings[].source
datasets[].entities[].aggregates[].required_filters
datasets[].entities[].aggregates[].required_filters[]
datasets[].entities[].aggregates[].source_entity
datasets[].entities[].aggregates[].spatial
datasets[].entities[].aggregates[].spatial.bbox_fields
datasets[].entities[].aggregates[].spatial.bbox_fields.max_x
datasets[].entities[].aggregates[].spatial.bbox_fields.max_y
datasets[].entities[].aggregates[].spatial.bbox_fields.min_x
datasets[].entities[].aggregates[].spatial.bbox_fields.min_y
datasets[].entities[].aggregates[].spatial.collection_id
datasets[].entities[].aggregates[].spatial.dimension
datasets[].entities[].aggregates[].spatial.geometry_entity
datasets[].entities[].aggregates[].spatial.geometry_field
datasets[].entities[].aggregates[].spatial.geometry_id_field
datasets[].entities[].aggregates[].spatial.max_geometry_vertices
datasets[].entities[].aggregates[].spatial.mode
datasets[].entities[].aggregates[].temporal_field
datasets[].entities[].aggregates[].title
datasets[].entities[].api
datasets[].entities[].api.allowed_expansions
datasets[].entities[].api.allowed_expansions[]
datasets[].entities[].api.allowed_filters
datasets[].entities[].api.allowed_filters[]
datasets[].entities[].api.allowed_filters[].field
datasets[].entities[].api.allowed_filters[].ops
datasets[].entities[].api.allowed_filters[].ops[]
datasets[].entities[].api.default_limit
datasets[].entities[].api.governed_policy
datasets[].entities[].api.governed_policy.allowed_assurance
datasets[].entities[].api.governed_policy.allowed_assurance[]
datasets[].entities[].api.governed_policy.max_source_age_seconds
datasets[].entities[].api.governed_policy.minimum_assurance
datasets[].entities[].api.governed_policy.permitted_jurisdictions
datasets[].entities[].api.governed_policy.permitted_jurisdictions[]
datasets[].entities[].api.governed_policy.permitted_purposes
datasets[].entities[].api.governed_policy.permitted_purposes[]
datasets[].entities[].api.governed_policy.redaction_fields
datasets[].entities[].api.governed_policy.redaction_fields[]
datasets[].entities[].api.governed_policy.require_consent
datasets[].entities[].api.governed_policy.require_legal_basis
datasets[].entities[].api.governed_policy.trusted_context
datasets[].entities[].api.governed_policy.trusted_context.asserted_assurance
datasets[].entities[].api.governed_policy.trusted_context.consent_ref
datasets[].entities[].api.governed_policy.trusted_context.jurisdiction
datasets[].entities[].api.governed_policy.trusted_context.legal_basis_ref
datasets[].entities[].api.governed_policy.trusted_context.source_observed_age_seconds
datasets[].entities[].api.max_limit
datasets[].entities[].api.require_purpose_header
datasets[].entities[].api.required_filter_bindings
datasets[].entities[].api.required_filter_bindings[]
datasets[].entities[].api.required_filter_bindings[].field
datasets[].entities[].api.required_filter_bindings[].source
datasets[].entities[].api.required_filters
datasets[].entities[].api.required_filters[]
datasets[].entities[].attribute_release_profiles
datasets[].entities[].attribute_release_profiles[]
datasets[].entities[].attribute_release_profiles[].claims
datasets[].entities[].attribute_release_profiles[].claims[]
datasets[].entities[].attribute_release_profiles[].claims[].expression
datasets[].entities[].attribute_release_profiles[].claims[].expression.cel
datasets[].entities[].attribute_release_profiles[].claims[].format
datasets[].entities[].attribute_release_profiles[].claims[].locale
datasets[].entities[].attribute_release_profiles[].claims[].name
datasets[].entities[].attribute_release_profiles[].claims[].required
datasets[].entities[].attribute_release_profiles[].claims[].sensitivity
datasets[].entities[].attribute_release_profiles[].claims[].shareable
datasets[].entities[].attribute_release_profiles[].claims[].source_field
datasets[].entities[].attribute_release_profiles[].description
datasets[].entities[].attribute_release_profiles[].id
datasets[].entities[].attribute_release_profiles[].purpose
datasets[].entities[].attribute_release_profiles[].release_conditions
datasets[].entities[].attribute_release_profiles[].release_conditions.denied_code
datasets[].entities[].attribute_release_profiles[].release_conditions.expression
datasets[].entities[].attribute_release_profiles[].release_conditions.expression.cel
datasets[].entities[].attribute_release_profiles[].release_scope
datasets[].entities[].attribute_release_profiles[].response
datasets[].entities[].attribute_release_profiles[].response.include_source_metadata
datasets[].entities[].attribute_release_profiles[].response.max_age_seconds
datasets[].entities[].attribute_release_profiles[].subject
datasets[].entities[].attribute_release_profiles[].subject.cardinality
datasets[].entities[].attribute_release_profiles[].subject.id_type
datasets[].entities[].attribute_release_profiles[].subject.input
datasets[].entities[].attribute_release_profiles[].subject.source_field
datasets[].entities[].attribute_release_profiles[].title
datasets[].entities[].attribute_release_profiles[].version
datasets[].entities[].concept_uri
datasets[].entities[].description
datasets[].entities[].fields
datasets[].entities[].fields[]
datasets[].entities[].fields[].codelist
datasets[].entities[].fields[].concept_uri
datasets[].entities[].fields[].from
datasets[].entities[].fields[].language
datasets[].entities[].fields[].name
datasets[].entities[].fields[].sensitive
datasets[].entities[].fields[].unit
datasets[].entities[].name
datasets[].entities[].relationships
datasets[].entities[].relationships[]
datasets[].entities[].relationships[].concept_uri
datasets[].entities[].relationships[].foreign_key
datasets[].entities[].relationships[].kind
datasets[].entities[].relationships[].name
datasets[].entities[].relationships[].target
datasets[].entities[].spatial
datasets[].entities[].spatial.bbox_fields
datasets[].entities[].spatial.bbox_fields.max_x
datasets[].entities[].spatial.bbox_fields.max_y
datasets[].entities[].spatial.bbox_fields.min_x
datasets[].entities[].spatial.bbox_fields.min_y
datasets[].entities[].spatial.collection_id
datasets[].entities[].spatial.datetime_field
datasets[].entities[].spatial.description
datasets[].entities[].spatial.geometry
datasets[].entities[].spatial.geometry.crs
datasets[].entities[].spatial.geometry.field
datasets[].entities[].spatial.geometry.kind
datasets[].entities[].spatial.geometry.latitude_field
datasets[].entities[].spatial.geometry.longitude_field
datasets[].entities[].spatial.max_bbox_degrees
datasets[].entities[].spatial.max_geometry_vertices
datasets[].entities[].spatial.title
datasets[].entities[].table
datasets[].entities[].title
datasets[].id
datasets[].owner
datasets[].public_services
datasets[].public_services[]
datasets[].public_services[].description
datasets[].public_services[].id
datasets[].public_services[].title
datasets[].sensitivity
datasets[].spatial_coverage
datasets[].status
datasets[].tables
datasets[].tables[]
datasets[].tables[].access
datasets[].tables[].access.aggregate_scope
datasets[].tables[].access.metadata_scope
datasets[].tables[].aggregates
datasets[].tables[].aggregates[]
datasets[].tables[].aggregates[].access
datasets[].tables[].aggregates[].access.aggregate_only_execution
datasets[].tables[].aggregates[].access.aggregate_scope
datasets[].tables[].aggregates[].access.metadata_scope
datasets[].tables[].aggregates[].allowed_filters
datasets[].tables[].aggregates[].allowed_filters[]
datasets[].tables[].aggregates[].allowed_filters[].field
datasets[].tables[].aggregates[].allowed_filters[].ops
datasets[].tables[].aggregates[].allowed_filters[].ops[]
datasets[].tables[].aggregates[].default_group_by
datasets[].tables[].aggregates[].default_group_by[]
datasets[].tables[].aggregates[].description
datasets[].tables[].aggregates[].dimensions
datasets[].tables[].aggregates[].dimensions[]
datasets[].tables[].aggregates[].dimensions[].codelist
datasets[].tables[].aggregates[].dimensions[].field
datasets[].tables[].aggregates[].dimensions[].id
datasets[].tables[].aggregates[].dimensions[].label
datasets[].tables[].aggregates[].disclosure_control
datasets[].tables[].aggregates[].disclosure_control.method
datasets[].tables[].aggregates[].disclosure_control.method[]
datasets[].tables[].aggregates[].disclosure_control.min_cell_size
datasets[].tables[].aggregates[].disclosure_control.min_group_size
datasets[].tables[].aggregates[].disclosure_control.report_suppressed_rows
datasets[].tables[].aggregates[].disclosure_control.suppression
datasets[].tables[].aggregates[].group_by
datasets[].tables[].aggregates[].group_by[]
datasets[].tables[].aggregates[].id
datasets[].tables[].aggregates[].indicators
datasets[].tables[].aggregates[].indicators[]
datasets[].tables[].aggregates[].indicators[].column
datasets[].tables[].aggregates[].indicators[].decimals
datasets[].tables[].aggregates[].indicators[].definition_uri
datasets[].tables[].aggregates[].indicators[].frequency
datasets[].tables[].aggregates[].indicators[].function
datasets[].tables[].aggregates[].indicators[].id
datasets[].tables[].aggregates[].indicators[].label
datasets[].tables[].aggregates[].indicators[].unit_measure
datasets[].tables[].aggregates[].indicators[].unit_mult
datasets[].tables[].aggregates[].joins
datasets[].tables[].aggregates[].joins[]
datasets[].tables[].aggregates[].joins[].relationship
datasets[].tables[].aggregates[].measures
datasets[].tables[].aggregates[].measures[]
datasets[].tables[].aggregates[].measures[].column
datasets[].tables[].aggregates[].measures[].function
datasets[].tables[].aggregates[].measures[].name
datasets[].tables[].aggregates[].required_filter_bindings
datasets[].tables[].aggregates[].required_filter_bindings[]
datasets[].tables[].aggregates[].required_filter_bindings[].field
datasets[].tables[].aggregates[].required_filter_bindings[].source
datasets[].tables[].aggregates[].required_filters
datasets[].tables[].aggregates[].required_filters[]
datasets[].tables[].aggregates[].source_entity
datasets[].tables[].aggregates[].spatial
datasets[].tables[].aggregates[].spatial.bbox_fields
datasets[].tables[].aggregates[].spatial.bbox_fields.max_x
datasets[].tables[].aggregates[].spatial.bbox_fields.max_y
datasets[].tables[].aggregates[].spatial.bbox_fields.min_x
datasets[].tables[].aggregates[].spatial.bbox_fields.min_y
datasets[].tables[].aggregates[].spatial.collection_id
datasets[].tables[].aggregates[].spatial.dimension
datasets[].tables[].aggregates[].spatial.geometry_entity
datasets[].tables[].aggregates[].spatial.geometry_field
datasets[].tables[].aggregates[].spatial.geometry_id_field
datasets[].tables[].aggregates[].spatial.max_geometry_vertices
datasets[].tables[].aggregates[].spatial.mode
datasets[].tables[].aggregates[].temporal_field
datasets[].tables[].aggregates[].title
datasets[].tables[].api
datasets[].tables[].api.allowed_filters
datasets[].tables[].api.allowed_filters[]
datasets[].tables[].api.allowed_filters[].field
datasets[].tables[].api.allowed_filters[].ops
datasets[].tables[].api.allowed_filters[].ops[]
datasets[].tables[].api.default_limit
datasets[].tables[].api.max_limit
datasets[].tables[].api.require_purpose_header
datasets[].tables[].id
datasets[].tables[].materialization
datasets[].tables[].primary_key
datasets[].tables[].refresh
datasets[].tables[].refresh.interval
datasets[].tables[].refresh.mode
datasets[].tables[].schema
datasets[].tables[].schema.fields
datasets[].tables[].schema.fields[]
datasets[].tables[].schema.fields[].codelist
datasets[].tables[].schema.fields[].concept_uri
datasets[].tables[].schema.fields[].language
datasets[].tables[].schema.fields[].name
datasets[].tables[].schema.fields[].nullable
datasets[].tables[].schema.fields[].sensitive
datasets[].tables[].schema.fields[].type
datasets[].tables[].schema.fields[].unit
datasets[].tables[].schema.strict
datasets[].tables[].source
datasets[].tables[].source.change_token_sql
datasets[].tables[].source.connect_timeout
datasets[].tables[].source.connection_env
datasets[].tables[].source.format
datasets[].tables[].source.format.csv
datasets[].tables[].source.format.csv.delimiter
datasets[].tables[].source.format.csv.header_row
datasets[].tables[].source.format.csv.quote
datasets[].tables[].source.format.parquet
datasets[].tables[].source.format.xlsx
datasets[].tables[].source.format.xlsx.data_range
datasets[].tables[].source.format.xlsx.header_row
datasets[].tables[].source.format.xlsx.sheet
datasets[].tables[].source.path
datasets[].tables[].source.query
datasets[].tables[].source.query_timeout
datasets[].tables[].source.table
datasets[].tables[].source.table.name
datasets[].tables[].source.table.schema
datasets[].tables[].source.type
datasets[].title
datasets[].update_frequency
deployment
deployment.evidence
deployment.evidence.api_key_rotation
deployment.evidence.audit_ack_cursor_path
deployment.evidence.audit_ack_max_age_secs
deployment.evidence.audit_offhost_shipping
deployment.evidence.ingress_rate_limit
deployment.profile
deployment.waivers
deployment.waivers[]
deployment.waivers[].expires
deployment.waivers[].finding
deployment.waivers[].reference
deployment.waivers[].summary
instance
instance.environment
instance.id
instance.jurisdiction
instance.owner
metadata
metadata.ecosystem_binding
metadata.ecosystem_binding.id
metadata.ecosystem_binding.version
metadata.source
metadata.source.digest
metadata.source.path
server
server.admin_bind
server.bind
server.cache_dir
server.cors
server.cors.allowed_origins
server.cors.allowed_origins[]
server.http1_header_read_timeout
server.max_connections
server.max_source_file_bytes
server.openapi_requires_auth
server.request_body_timeout
server.request_timeout
server.trust_proxy
server.trust_proxy.enabled
server.trust_proxy.trusted_proxies
server.trust_proxy.trusted_proxies[]
server.xlsx_max_file_bytes
standards
standards.spdci
standards.spdci.disability_registry
standards.spdci.disability_registry.dataset
standards.spdci.disability_registry.disabled_positive_values
standards.spdci.disability_registry.disabled_positive_values[]
standards.spdci.disability_registry.disabled_status_field
standards.spdci.disability_registry.entity
standards.spdci.disability_registry.query_field
standards.spdci.disability_registry.query_key
standards.spdci.registries
standards.spdci.registries.*
standards.spdci.registries.*.dataset
standards.spdci.registries.*.default_limit
standards.spdci.registries.*.entity
standards.spdci.registries.*.expression_fields
standards.spdci.registries.*.expression_fields.*
standards.spdci.registries.*.identifiers
standards.spdci.registries.*.identifiers.*
standards.spdci.registries.*.record_type
standards.spdci.registries.*.registry_type
standards.spdci.registries.*.response_fields
standards.spdci.registries.*.response_fields.*
standards.spdci.registries.*.response_mapping_path
standards.spdci.registries.*.response_schema_path
vocabularies
vocabularies.*
```
{/* registry-relay-config-key-paths:end */}

`registry-relay` is configured by one YAML document. The binary chooses the first available source:

1. `--config <path>`
2. `REGISTRY_RELAY_CONFIG`
3. `./config/example.yaml`

The canonical sample is [config/example.yaml](https://github.com/registrystack/registry-stack/blob/d45761a0104bd3d9c2e4b4db391d4223f289bd44/crates/registry-relay/config/example.yaml). Keep examples aligned with this guide and the API and operations documentation.

## Root shape

```yaml
instance: {}
server: {}
metadata: {}   # optional split portable metadata manifest
catalog: {}
vocabularies: {}
auth: {}
audit: {}
consultation: {} # optional restart-only purpose-aware consultation runtime
deployment:
  profile: local # required; use local only for development
config_trust: {} # optional signed bundle boot trust
datasets: []
standards: {}  # optional, feature-gated adapters
```

Unknown fields are rejected for most blocks. Config validation runs after YAML parsing and checks ids, scopes, table/entity references, filter references, aggregate references, env var presence, and vocabulary prefixes.

The complete deserialization-oriented Draft 2020-12 schema is committed at
[`schemas/registry-relay.config.schema.json`](https://github.com/registrystack/registry-stack/blob/d45761a0104bd3d9c2e4b4db391d4223f289bd44/schemas/registry-relay.config.schema.json).
Reproduce it from this directory with `just config-schema-generate`, verify
drift with `just config-schema-check`, or print the exact same bytes with
`registry-relay schema --format json`. The schema checks document structure,
closed objects, tagged variants, scalar shapes, and constrained reference
syntax. `registry-relay doctor` remains authoritative for environment and
secret availability, filesystem and source access, activation rules, and
cross-field runtime validation.

## Environment expansion

Relay expands `${VAR}` expressions before YAML parsing. `${VAR}` requires
`VAR` to be set to a non-empty value. `${VAR:-fallback}` uses `fallback` when
`VAR` is unset or empty, including `${VAR:-}` for an explicit empty result.
`${VAR:?message}` fails with `message` when `VAR` is unset or empty.
Whitespace-only values are non-empty. Diagnostics name the variable or use the
supplied message; they never include the variable value.

Environment-reference fields follow the invariant of their runtime consumer:

- `auth.api_keys[].fingerprint.name` accepts any non-empty operating-system
  environment name except names containing `=` or NUL. It does not impose an
  identifier grammar or a 128-byte limit, and its consumer permits
  whitespace-only names.
- `audit.hash_secret_env` uses the same operating-system name rules but must
  contain at least one non-whitespace character, matching the audit runtime's
  fail-closed empty-name check. Names containing dots or hyphens remain valid.
- Postgres `connection_env` uses `[A-Za-z_][A-Za-z0-9_]*`, matching source
  validation, without an artificial length limit.
- Consultation database, credential, and pseudonym secret references use the
  portable `[A-Za-z_][A-Za-z0-9_]{0,127}` grammar enforced during
  deserialization and consultation operations.

A minimal entity-serving deployment needs `server` (a listener), `catalog` (public metadata base), `auth` (one auth mode), `audit` (a sink and hash secret), and at least one entry in `datasets`. A consultation-only deployment can use `datasets: []` when the complete `consultation` block activates at least one profile.
Every other root block is optional.
This example shows the required shape.
For a runnable starting point, use `config/example.yaml`.
Env-backed API key configs name the secret-store entry that contains the canonical fingerprint.

```yaml
server:
  bind: 127.0.0.1:8080

catalog:
  title: Example Registry Relay
  base_url: http://127.0.0.1:8080
  publisher: Example Ministry

auth:
  mode: api_key
  api_keys:
    - id: demo_client
      fingerprint:
        provider: env
        name: API_KEY_HASH
      scopes:
        - people:metadata
        - people:rows

audit:
  sink: stdout
  hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET

datasets:
  - id: people
    title: People registry
    description: Demo people records
    owner: Example Ministry
    sensitivity: personal
    access_rights: restricted
    update_frequency: monthly
    tables:
      - id: people_table
        source:
          type: file
          path: ./data/people.csv
          format:
            csv:
              header_row: 1
        primary_key: person_id
        schema:
          strict: true
          fields:
            - name: person_id
              type: string
              nullable: false
            - name: name
              type: string
              nullable: false
    entities:
      - name: person
        table: people_table
        fields:
          - name: person_id
          - name: name
        access:
          metadata_scope: people:metadata
          aggregate_scope: people:metadata
          read_scope: people:rows
        api:
          default_limit: 50
          max_limit: 100
```

The `API_KEY_HASH` environment variable must contain a canonical fingerprint in the form `sha256:<64 lowercase hex chars>`.
The raw API key stays outside the config and is given only to the authorized client.
The `REGISTRY_RELAY_AUDIT_HASH_SECRET` environment variable must contain at least 32 bytes of random secret material; startup fails closed when it is absent or weak.

See [config/example.yaml](https://github.com/registrystack/registry-stack/blob/d45761a0104bd3d9c2e4b4db391d4223f289bd44/crates/registry-relay/config/example.yaml) for a larger working starting point; the sections that follow document each block in full.

## Purpose-aware consultations

`consultation` activates Relay's restart-only native API for one exact authorized
workload and the exact profiles in one hash-pinned artifact closure. It
is all-or-nothing: Relay refuses startup when OIDC, workload identity, artifact
closure, state-plane identity, pseudonym material references, source credential
references, or compiled plan support are incomplete or inconsistent.

Use the generic [Registry Stack project-authoring workflow](https://docs.registrystack.org/tutorials/author-registry-project/)
to produce the complete Relay input for a deployment. The maintained
[DHIS2 journey](https://github.com/registrystack/registry-stack/blob/d45761a0104bd3d9c2e4b4db391d4223f289bd44/crates/registry-relay/profiles/dhis2-2.41.9-enrollment-status/relay-config.example.yaml)
is one interoperability example, not a source-product or version-specific
runtime shape. Its generated config keeps exact typed artifact hashes and raw
file digests next to that example.

`authorized_workload` fixes one OIDC client identity. The issuer comes from
`auth.oidc`; the audience and selected `azp` or `client_id` must agree with the
OIDC client allowlist. Each compiled public contract adds its own exact scope,
purpose, tenant, registry, and source-plan binding.

`state_plane.database_url_env` names the environment variable containing the
PostgreSQL runtime connection URL. The optional `root_certificate_path` pins a
private PostgreSQL trust root exclusively: when configured, Relay does not
also trust system roots. Omit it to use the host's normal system trust store.
Relay rejects a custom root on Android because that platform's `native-tls`
backend cannot exclude the Android system root directory.
The epoch id and advisory-lock keys are stable, deployment-owned identifiers
and must not collide with another state-plane user. Do not place a database URL
in YAML.

`artifacts` is a complete catalog. Every public contract listed there is an
enabled consultation. Local development pins every file with both its typed
artifact hash where applicable and its raw SHA-256 digest. Non-local profiles
must receive the files through the verified signed Config Bundle path. Relay
does not discover, download, or hot-reload consultation profiles.

Each private destination binding defaults to
`dns_family: dual_stack_strict`: Relay requires definitive A and AAAA lookup
outcomes before it connects. For a domain destination intentionally operated
over IPv4, set `dns_family: ipv4_only` on that destination. This is an A-only
security policy, not a fallback preference. It never queries or accepts IPv6,
still re-resolves and validates the complete A answer set for every call, and
rejects literal origins or IPv6 private CIDRs. The selected mode is covered by
the private-binding hash, so changing it requires reviewing and repinning that
binding.

`audit_pseudonym_materials` and `source_credentials` contain environment
references only. An audit key id is immutable: replace material under a new id
instead of changing bytes behind an existing id. A source credential generation
is also explicit and positive. V1 supports environment-backed HTTP Basic source
credentials for the concrete maintained DHIS2 journey. Secret values must not
appear in YAML, diagnostics, logs, or evidence.

Run `registry-relay doctor --config <path> --profile <profile> --format json`
before bootstrap or startup. Consultation activation is restart-only, so deploy
the reviewed config and artifact closure as one unit.

## Instance

```yaml
instance:
  id: registry-relay-local
  environment: development
  owner: Ministry of Digital Government
  jurisdiction: example-country
```

`instance` gives posture and operations tooling a stable public identity for the
running service. `id` defaults to `registry-relay-local`; `environment`, `owner`,
and `jurisdiction` are optional public labels.

## Server

```yaml
server:
  bind: 0.0.0.0:8080
  admin_bind: 127.0.0.1:8081
  openapi_requires_auth: true
  cache_dir: ./cache
  max_source_file_bytes: 268435456
  xlsx_max_file_bytes: 268435456
  request_timeout: 30s
  request_body_timeout: 10s
  http1_header_read_timeout: 10s
  max_connections: 1024
  cors:
    allowed_origins:
      - https://portal.example.gov
  trust_proxy:
    enabled: false
    trusted_proxies: []
```

`bind` is the public data-plane listener.
`admin_bind` is optional and must be private in production.
Listener addresses use canonical dotted-decimal IPv4 or bracketed hexadecimal
IPv6 followed by a canonical decimal port from `0` through `65535`.
IPv4-embedded IPv6 and IPv6 zone identifiers are outside this portable config
grammar.
`cache_dir` must be writable by the process.
Source data must be mounted read-only.

`openapi_requires_auth` defaults to `true`. Set it to `false` only for local testing or controlled tooling environments that need unauthenticated access to `/openapi.json`; the unauthenticated document includes the full configured OpenAPI surface.

`request_timeout` bounds total request service time after HTTP headers are parsed. `request_body_timeout` bounds body reads for handlers that consume a request body. `http1_header_read_timeout` closes incomplete HTTP/1 headers before request work is admitted, and `max_connections` caps concurrent accepted sockets per listener. All timeouts must be non-zero and `max_connections` must be greater than zero.

Every duration field uses the same stable humantime subset.
A value contains one or more non-negative integer components of at most 10
digits, separated by one ASCII space, with units `ns`, `us`, `ms`, `s`, `m`,
`h`, `d`, or `w`.
The complete value is at most 255 bytes.
Examples include `30s`, `10m`, `1h`, and `2h 37m`.
Bare numbers, negative or fractional components, long unit aliases, adjacent
components such as `1h30m`, and repeated spaces are rejected by both runtime
deserialization and the JSON Schema.

HTTP/2 connections use the same finite connection cap and keepalive timeout. If production terminates HTTP/2 at a reverse proxy, configure bounded proxy header/body read timeouts and per-client connection limits before forwarding to Registry Relay.

The default CORS policy is deny by omission. Add explicit trusted origins only.

## Config Bundle Trust

Most deployments can skip this section. `config_trust` is optional; it makes
startup config come from a signed, local config bundle. Simple local deployments
omit it and keep using the local YAML loaded at startup.

This example is syntactically valid but illustrative. Generate the trust anchor
and signed bundle with `registryctl anchor` and `registryctl bundle` before using
it in an environment.

```yaml
config_trust:
  trust_anchor_path: /etc/registry-relay/config/trust-anchor.json
  bundle_path: /etc/registry-relay/config/bundle
  antirollback_state_path: /var/lib/registry-relay/config-state/antirollback.json
  break_glass_override_path: /run/registry-relay/config-override.json
```

Config bundle trust is boot-time only. Relay reads no remote metadata, exposes
no admin config apply endpoint, and does not hot-apply runtime config. At boot it
verifies the anchor permissions, the bundle manifest and signature, product and
environment binding, bundle file closure, anti-rollback sequence, and full Relay
config validation. The accepted bundle is audited before the anti-rollback state
is advanced.

`antirollback_state_path` must point to durable local state such as a mounted
volume. `break_glass_override_path` is optional and points to a root-owned
one-shot override file. Rollback overrides may accept the exact signed bundle
hash named by the file. `accept_unsigned` overrides may pin an absolute local
config path and hash for emergency startup; signature, binding, and sequence
checks are skipped, but file permissions, hash pinning, and Relay config
validation still run.

## Catalog and vocabularies

```yaml
catalog:
  title: Internal Government Registry Relay
  base_url: https://data.example.gov
  publisher: Ministry of Digital Government
  participant_id: did:web:data.example.gov

vocabularies:
  psc: https://publicschema.org/
  m8g: http://data.europa.eu/m8g/
```

`base_url` is used in generated catalog links and OpenAPI servers. `participant_id` is optional and defaults from the catalog base URL when omitted.

Vocabulary prefixes let entity fields and dataset metadata use compact semantic references such as `psc:concepts/Person`.

## Split metadata manifest

```yaml
metadata:
  source:
    path: ./metadata.yaml
```

`metadata.source.path` points at a portable metadata manifest. Relative paths
are resolved from the runtime config file. At startup, Registry Relay compiles
the manifest and validates that runtime datasets, entities, fields, filters, and
relationships are present in the metadata model. Add
`metadata.source.digest: sha256:<digest>` when the deployment must pin the
exact reviewed manifest.

| Mode | Required config | Digest rule | Delivery |
| --- | --- | --- | --- |
| Simple local | `metadata.source.path` | Optional | Local file read at startup |
| Pinned local | `metadata.source.path`, `metadata.source.digest` | Must match the local manifest | Local file read at startup |
| Governed | `config_trust`, `metadata.source.path`, `metadata.source.digest` | Required before startup | Signed config target plus signed metadata target; optional signed package index when `package_digest` is claimed |

Keep operational details in this runtime config: sources, tables, physical
columns, scopes, filters, aggregates, standards adapters, ingest, and refresh.
Keep standard-facing meaning in the manifest: catalog, datasets, entities,
fields, constraints, vocabularies, codelists, profiles, conformance claims, and
descriptive ODRL policy metadata.

See [metadata.md](../metadata/) for the manifest schema, static publication, and
the `metadata.manifest.*` / `runtime.binding.*` startup error codes.

ODRL policy belongs in the portable metadata manifest, not in runtime dataset
bindings. A dataset `policy` block is published as an `odrl:Offer` for discovery
and review evidence. When a runtime config selects a governed ecosystem binding,
Relay also uses supported metadata purpose constraints as governed PDP purpose
constraints on entity-derived evidence routes. The metadata policy still does not
grant API-key scopes, OIDC roles, row filters, evidence verification privileges,
or SP DCI access by itself.

```yaml
metadata:
  source:
    path: ./disability_registry.metadata.yaml

# In disability_registry.metadata.yaml:
datasets:
  - id: disability_registry
    policy:
      uid: https://demo.example.gov/datasets/disability_registry#illustrative-offer
      assigner: did:web:social-affairs.demo.example.gov
      permissions:
        - action: odrl:use
          constraints:
            - left_operand: odrl:purpose
              operator: odrl:isA
              right_operand:
                iri: https://demo.example.gov/purpose/disability-benefit-eligibility
          duties:
            - action: odrl:attribute
      prohibitions:
        - action: odrl:sell
```

The demo policy IRIs under `demo.example.gov` are hypothetical examples for
catalog consumers. They are not official policy, legal advice, or a declaration
that a client has been approved to use the data.

## SP DCI sync adapter

SP DCI (the Social Protection Digital Convergence Initiative) sync adapters are optional and feature-gated. Build with `--features spdci-api-standards` to enable them. Without that feature, any `standards.spdci` config is rejected with `spdci.config.feature_disabled`.

The adapter does not add new storage semantics. Configure a normal Registry Relay entity, often backed by an XLSX worksheet, then bind the SP DCI sync routes to it:

```yaml
standards:
  spdci:
    disability_registry:
      dataset: disability_registry
      entity: disabled_person
      query_key: member.member_identifier
      query_field: id
      disabled_status_field: disability_status
      disabled_positive_values: [approved, yes]
    registries:
      dr:
        dataset: disability_registry
        entity: disabled_person
        registry_type: ns:org:RegistryType:DR
        record_type: spdci-extensions-dci:DisabledPerson
        identifiers:
          DISABILITY_ID: id
          MEMBER_ID: id
        expression_fields:
          disability_status: disability_status
          disability_details.impairment_type: impairment_type
```

When enabled and configured, Registry Relay serves these SP DCI sync endpoints on the protected data-plane listener:

```text
POST /dci/{registry}/registry/sync/search
POST /dci/{registry}/registry/sync/disabled
POST /dci/{registry}/registry/sync/get-disability-details
POST /dci/{registry}/registry/sync/get-disability-support
```

For `sync/search`, the `{registry}` segment selects any named `standards.spdci.registries` entry such as `dr`, `sr`, `crvs`, or `fr`, which lets one listener host multiple DCI registry APIs without path ambiguity. The `disabled`, `get-disability-details`, and `get-disability-support` routes are Disability Registry-specific and resolve only when the named registry entry points at the same dataset/entity as `standards.spdci.disability_registry`. The async `/registry/search`, subscribe, callback, and transaction-status APIs are intentionally not implemented by this sync adapter.

For generic sync search, `identifiers` maps DCI `idtype-value` query types to entity fields. `expression_fields` maps DCI expression or predicate attribute names to entity fields. Mapped fields must be exposed entity fields and allowed filters. The adapter currently supports `idtype-value`, expression `$and` with `eq`, `in`, `ge`, and `le`, and predicate conditions joined with `and`.

`query_key` is read from `message.disabled_criteria.query` in the SP DCI request envelope. It may be represented as a literal dotted JSON key (`"member.member_identifier"`) or as nested objects (`{"member": {"member_identifier": ...}}`). `query_field` must be an allowed entity filter because the adapter delegates reads to the normal entity query engine.

For `/dci/{registry}/registry/sync/disabled`, the caller needs the entity `evidence_verification_scope`. Generic search, details, and support need the entity `read_scope`. API-key authentication is still Registry Relay's normal auth layer. If a registry entry uses `response_mapping_path`, the binary must also be built with `--features standards-cel-mapping`; otherwise config validation fails with `spdci.config.mapping_feature_disabled`.

## API keys

```yaml
auth:
  mode: api_key
  api_keys:
    - id: program_system
      fingerprint:
        provider: env
        name: PROGRAM_SYSTEM_API_KEY_HASH
      scopes:
        - social_registry:metadata
        - social_registry:rows
```

The YAML stores fingerprint references, never raw API keys.
Each env var value must be:

```text
sha256:<64 lowercase hex chars>
```

Generate a raw key and its fingerprint:

```sh
registry-relay generate-api-key --id program_system
```

The command emits three shell-friendly lines:

```text
api_key_id=program_system
api_key=<send-this-raw-key-to-the-client>
fingerprint=sha256:<store-this-in-the-secret-store>
```

Store the emitted fingerprint in the platform secret store under the configured `fingerprint.name`.
Give the raw key only to the authorized client.
Restart Registry Relay, or apply a governed config change that points at a new immutable or versioned fingerprint reference, before expecting the new credential to authenticate.

Worked standalone example, using `demo_client` and `API_KEY_HASH`:

```text
api_key_id=demo_client
api_key=registry-relay-standalone-example-key-0001
fingerprint=sha256:db3f2a02c6ead9bf0387e8a97ec090a549daa46610ca87bd4b651631b2411def
```

```sh
export API_KEY_HASH='sha256:db3f2a02c6ead9bf0387e8a97ec090a549daa46610ca87bd4b651631b2411def'
```

```yaml
auth:
  mode: api_key
  api_keys:
    - id: demo_client
      fingerprint:
        provider: env
        name: API_KEY_HASH
      scopes:
        - people:metadata
        - people:rows
```

Do not reuse the example raw key in a real deployment.

## OIDC (OAuth2)

Set `auth.mode: oidc` to verify bearer JWTs against an external OpenID Connect / OAuth2 IdP. Registry Relay is a resource server: it validates inbound tokens against the IdP's JWKS but never mints, refreshes, or stores tokens. A given deployment runs in exactly one auth mode at a time; mixed-mode operation is not supported.

OIDC field names follow the shared Registry service runtime configuration conventions.
Field names from earlier releases are rejected with an error naming the replacement field.

```yaml
auth:
  mode: oidc
  oidc:
    issuer: https://idp.example.gov
    audiences:
      - registry-relay
    discovery_url: https://idp.example.gov/.well-known/openid-configuration
    allowed_algorithms:
      - RS256
    jwks_cache_ttl: 10m
    leeway: 60s
    scope_claim: scope
    scope_map:
      "role:social-registry-reader": "social_registry:rows"
    scope_object_required_keys: []
    allowed_clients:
      - registry-relay-client
    allowed_token_types:
      - JWT
      - at+jwt
```

A full drop-in alternative to `config/example.yaml` lives at `config/example.oidc.yaml`. It targets a local Zitadel instance.

| Field             | Purpose                                                                                                                                                       |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `issuer`          | Compared verbatim against the JWT `iss` claim. Must match the IdP's published issuer URL.                                                                     |
| `audiences`       | One or more accepted `aud` values. Tokens whose `aud` does not intersect this list are rejected.                                                              |
| `jwks_url`        | Explicit JWKS endpoint. Exactly one of `jwks_url` and `discovery_url` must be set; the validator rejects configs that supply both or neither.                 |
| `discovery_url`   | OIDC discovery document (`.well-known/openid-configuration`). The JWKS URL is resolved from `jwks_uri` at startup.                                            |
| `allow_dev_insecure_fetch_urls` | Development-only opt-in for loopback HTTP issuer, discovery, and JWKS URLs. Defaults to `false`; non-loopback private and metadata IPs remain denied by the platform fetch policy. |
| `allowed_algorithms`      | Signature algorithms accepted by the verifier. RS256, ES256, EdDSA. HS\* and `none` are intentionally absent.                                                 |
| `jwks_cache_ttl`  | Steady-state JWKS cache TTL. The cache also refreshes on unknown `kid` (rate-limited), so this is the rotation pickup latency, not the upper bound.           |
| `leeway`          | Clock skew tolerance on `exp` and `nbf`. Bounded at 5 minutes by validation.                                                                                  |
| `scope_claim`     | Name of the JWT claim to read scopes from (the config field itself is always a single string; defaults to `scope`). The claim's *value* in the token may be a space-separated string (RFC 8693 / RFC 9068), a JSON array of strings, or a JSON object whose keys are the scope names. The `aud` claim is rejected as a scope source because it is used only for token audience validation. Object-valued role keys grant scopes only when `scope_object_required_keys` names a key present in the role value and that nested value is active: `true`, a non-empty string, or a non-empty object/array containing an active value. |
| `scope_map`       | Optional rename map applied before scope-based access checks. Adapt IdP role names to Registry Relay's `<dataset_id>:<level>` shape.                               |
| `scope_object_required_keys` | Allowlist of keys that must appear inside object-valued role claim values before the role key is accepted. For Zitadel organization-scoped role objects, set this to the expected organization id key or keys. Defaults to empty, which means object-valued claims grant no scopes. String and array scope claims do not require this setting. |
| `allowed_clients` | Optional allowlist matched against the token's `azp` (preferred) or `client_id`. Empty list means any client is accepted and is intended only for tightly controlled development. |
| `allowed_token_types`     | Accepted JOSE `typ` header values. Defaults to `JWT` and `at+jwt` (RFC 9068). ID tokens (`id+jwt`) are intentionally rejected by default, and tokens without `typ` are rejected by the shared verifier. |

### Discovery vs explicit JWKS

`discovery_url` triggers a single discovery fetch at startup to resolve `jwks_uri`; a failure here aborts the binary so an operator sees the IdP wiring problem instead of a process that runs but silently rejects every token. The JWKS document itself is fetched lazily on first verify, so a transient JWKS outage at boot does not block startup. Production defaults require HTTPS; local loopback HTTP requires `allow_dev_insecure_fetch_urls: true`.

### Resource-server semantics

Registry Relay never mints or refreshes tokens. Operators are responsible for provisioning OIDC applications, machine users, and grant types on the IdP. The Principal's `principal_id` is taken from the token's `sub` (preferred), then `client_id`, then `azp`; `auth_mode=oidc` is recorded on every audit record.

### Granular failure codes

Token verification failures map to specific `auth.*` codes so audit pipelines can distinguish IdP outages from bad tokens from policy denials:

| Code                            | HTTP | Meaning                                                       |
| ------------------------------- | ---- | ------------------------------------------------------------- |
| `auth.missing_credential`       | 401  | No `Authorization` header                                     |
| `auth.malformed_credential`     | 401  | Wrong scheme, empty bearer, or unparseable JWT structure      |
| `auth.token_expired`            | 401  | `exp` claim is in the past (after `leeway`)                   |
| `auth.token_not_yet_valid`      | 401  | `nbf` claim is in the future (after `leeway`)                 |
| `auth.token_signature_invalid`  | 401  | JWKS key found but signature did not verify                   |
| `auth.issuer_mismatch`          | 401  | `iss` claim does not match `oidc.issuer`                      |
| `auth.audience_mismatch`        | 401  | `aud` claim does not intersect `oidc.audiences`                |
| `auth.kid_unknown`              | 401  | Header `kid` is absent from the JWKS even after one refresh   |
| `auth.algorithm_not_allowed`    | 401  | Header `alg` is not in the configured allowlist               |
| `auth.client_not_allowed`       | 403  | `azp` / `client_id` is not in the configured `allowed_clients`|
| `auth.invalid_credential`       | 401  | JWT decode failure not covered by a more specific variant      |
| `auth.jwks_unavailable`         | 503  | JWKS fetch failed; Registry Relay cannot verify any token     |
| `auth.rate_limited`             | 429  | Local auth-failure throttle tripped for this client address (see below) |

For a worked example of running Registry Relay against a local OIDC provider (using the project's dev Zitadel stack), see [development.md](https://github.com/registrystack/registry-stack/blob/d45761a0104bd3d9c2e4b4db391d4223f289bd44/crates/registry-relay/docs/development.md).

## Auth-failure throttle

```yaml
auth:
  failure_throttle:
    enabled: false
    max_failures: 20
    window_seconds: 60
```

| Field             | Purpose                                                                                                             |
| ----------------- | --------------------------------------------------------------------------------------------------------------------- |
| `enabled`         | Off by default. When `false`, the throttle is never constructed and every request behaves exactly as it did before this feature existed. |
| `max_failures`    | Number of authentication failures allowed from one client address within `window_seconds` before further requests from that address are throttled. Must be greater than 0 when `enabled: true`. |
| `window_seconds`  | Fixed-window length in seconds. Must be greater than 0 when `enabled: true`.                                       |

This is a local, in-process, coarse throttle applied in front of the auth provider (API-key or OIDC), keyed on the same trust-proxy-aware client address the audit record's `remote_addr` field reports (`server.trust_proxy`). Once an address has reached `max_failures` failed authentication attempts within the window, every further request from that address (including ones presenting a valid credential) is short-circuited with a 429 and the stable code `auth.rate_limited`, plus a `Retry-After` header giving the remaining window in seconds, without invoking the auth provider. Successful authentication neither counts toward the limit nor resets it. The counter is process-local and bounded (a capped map with eviction of the oldest entry), so it recovers automatically on restart and cannot grow without bound under a flood of spoofed source addresses. The throttled short-circuit itself is audited like any other auth failure, with `error_code: auth.rate_limited` and `status_code: 429`.

Because the throttle key is the resolved client address, deploying behind a proxy or load balancer without effective trust-proxy support makes every request resolve to the proxy's own socket address, so all clients share one bucket. Combined authentication failures from any client then reach `max_failures` and 429 everyone, including callers presenting valid credentials, until the window rolls. Trust-proxy support is only effective when `server.trust_proxy.enabled` is true *and* `server.trust_proxy.trusted_proxies` names at least one proxy: an empty `trusted_proxies` list matches no peer, so `X-Forwarded-For` is ignored and the shared-bucket collapse still applies even with `enabled` set. Startup validation emits a `config.validation_warning` finding for both cases (trust_proxy disabled, or enabled with an empty `trusted_proxies` list); enable `server.trust_proxy` and populate `trusted_proxies` with the proxy address when the relay sits behind one.

### Denial-of-service posture

Ingress rate limiting (a load balancer, API gateway, or reverse proxy in front of Registry Relay) is the primary control for absorbing high-volume or distributed abuse; deployment profiles that lack one surface the `relay.ingress.rate_limit_missing` finding (see `deployment.evidence.ingress_rate_limit` below). `auth.failure_throttle` is a local backstop scoped narrowly to repeated authentication failures from a single address, useful for deployments without a gateway in front of them, or as defense in depth behind one. It does not protect other expensive routes (aggregation, large collection scans) from abuse by *authenticated* callers; throttling those routes is deliberately deferred to a future iteration and is not addressed by this feature.

## Audit

```yaml
audit:
  sink: stdout
  format: jsonl
  hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET
  chain: true
  include_health: false
```

`include_health` controls audit records for `/healthz`. Registry Relay always
excludes `/ready`: evidence-grade readiness requires the shipper cursor to equal
the live audit-chain tail, so appending a record after that comparison would
make a successful probe invalidate the next probe.

Supported sinks:

```yaml
audit:
  sink: file
  format: jsonl
  hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET
  path: /var/log/registry-relay/audit.jsonl
  rotate:
    max_size_mb: 100
    max_files: 14
```

```yaml
audit:
  sink: syslog
  format: jsonl
  hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET
```

`hash_secret_env` is required at runtime and must be a non-whitespace environment variable name containing no `=` or NUL. The named variable must contain at least 32 bytes of deployment-specific random secret material. Startup fails closed when the name is missing or whitespace-only, or when the variable is unset, empty, or weak.

Registry Relay uses this secret to pseudonymize sensitive audit handles. Values for
configured sensitive fields, record primary keys, table identifiers, and
attribute-release subject identifiers, when the off-by-default feature is
enabled, are written as stable audit hashes instead of raw strings. This gives
an auditor a way to see that the same subject or source was accessed more than
once without storing the underlying person identifier, address, date of birth,
or table id in the audit sink.

The handles are stable only for the same hash secret and audit hash domain. If
you rotate the secret, retain the old secret under your audit retention controls
for any period when older records must remain comparable, or accept that new
records will not match old handles.

Audit output uses `registry-platform-audit` envelopes with `prev_hash` and `record_hash` on every record. These fields detect edits, reordering, and gaps inside the retained log set, starting from the first retained record. They do not prove that earlier records were never deleted, or protect against an actor who can rewrite the entire local sink. Use off-host audit shipping when completeness matters. `chain` is retained in config for compatibility with older deployments, but platform audit envelopes are always chained.

A normally booted relay always reports keyed integrity `hmac` in its posture because startup requires the audit hash secret (`hash_secret_env`); the `none` value appears only in dev or test configurations that build the posture without that secret.

Audit records are separate from operational logs, which go to stderr as readable text by default. Set `REGISTRY_RELAY_LOG_FORMAT=json` or `REGISTRY_RELAY_LOG_FORMAT=jsonl` when operational logs are emitted as JSON Lines for collection or redirected files.

### Write policy

`write_policy` selects what happens when an audit record cannot be written (for example the sink is unreachable or the disk is full):

```yaml
audit:
  sink: file
  hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET
  path: /var/log/registry-relay/audit.jsonl
  write_policy: fail_closed   # fail_closed | availability_first
```

* `fail_closed` (default): a request whose audit record cannot be written fails with HTTP `503` and the stable error code `audit.write_failed` (`application/problem+json`). No request outcome is returned without a durable audit record.
* `availability_first`: an audit write failure is logged and the request returns its original outcome unchanged. The deployment stays available even when audit is degraded. Use this only when an explicit availability exception accepts best-effort audit durability.

The policy applies to every audited route. Per-route-family selection is not configurable. The selected policy is reported truthfully as the `write_policy` fact in the operations posture audit block, so a deployment cannot claim a stronger guarantee than it runs.

## Deployment profile

The `deployment` block lets an operator declare the assurance level a deployment claims. The profile is never inferred from hostname, environment, or network position: it is an explicit statement. Each profile binds a set of gates that check the running configuration and contribute findings at a defined severity.

```yaml
deployment:
  profile: production        # local | hosted_lab | production | evidence_grade
  evidence:
    ingress_rate_limit: true # operator asserts a gateway enforces rate limiting
    api_key_rotation: true   # operator asserts an API-key rotation process exists
    audit_offhost_shipping: true # operator asserts audit records are shipped off-host
    audit_ack_cursor_path: /var/lib/registry-relay/audit-ack-cursor.json # local state file the shipper updates
    audit_ack_max_age_secs: 900 # how old acked_at may get before the cursor reads as stale
  waivers:
    - finding: relay.openapi.public
      reference: OPS-2026-0042
      summary: Public API catalog is intentional for this deployment
      expires: 2026-12-31
```

`deployment.profile` is required at startup. Use `local` as the explicit development opt-out, or declare `hosted_lab`, `production`, or `evidence_grade` for deployed environments. When the profile is omitted, startup fails with `deployment.profile_undeclared`. An unknown profile value is rejected at startup.

### Profiles and severities

Each gate maps to one of four severities per profile:

* `startup_fail`: the process refuses to start. Never waivable.
* `readiness_fail`: the readiness endpoint reports not-ready; the process keeps running. Never waivable.
* `finding_error` / `finding_warn`: a posture finding only.

The four profiles escalate from `local` (binds no hard gates) through `hosted_lab` and `production` to `evidence_grade` (the strictest). For example, `evidence_grade` requires a signed, governed config bundle: running it from a plain local YAML file trips a `startup_fail` gate (`relay.config.unsigned`) and the process refuses to start.

### Evidence declarations

Some controls live outside the relay and cannot be observed by the process (for example ingress rate limiting enforced by a gateway, an API-key rotation process, or audit records shipped off-host to a log collector or SIEM). The `evidence` flags let the operator assert those controls are in place. Each flag defaults to `false`, which leaves the corresponding gate active until the operator declares the control.

`audit_ack_cursor_path` and `audit_ack_max_age_secs` are not booleans: they point at the regular, non-symlink state file a trusted off-host shipper atomically replaces after each successful hand-off (the `registry.audit.ack_cursor.v1` contract: `acked_at`, `last_acked_hash`, optional `writer`; maximum 16 KiB) and set how old `acked_at` may get before it reads as stale (defaults to 900 seconds). Mount the cursor read-only for Relay and keep it on local storage. Runtime health is `ok` only when the timestamp is fresh and the watermark equals the live keyed chain tail. Public readiness and posture reads use one blocking worker with a 500 ms deadline; a stalled read fails closed without queuing more readers. Config load rejects `audit_ack_max_age_secs` without a cursor path, and rejects a cursor on a local `file` sink without `audit_offhost_shipping`. `stdout` and `syslog` do not need that declaration, but evidence-grade policy still requires their cursor so shipping progress is observed.

### Waivers

A triggered, waivable finding can be suppressed by a waiver that names the finding id, carries a required operator reference, and sets a mandatory expiry date (`YYYY-MM-DD`). An optional summary can add short operational context:

```yaml
deployment:
  profile: hosted_lab
  waivers:
    - finding: relay.ingress.rate_limit_missing
      reference: OPS-2026-0042
      summary: Rate limiting is handled by the lab gateway
      expires: 2026-09-30
```

A waived finding reports status `waived` instead of its severity effect.
Once the expiry date passes, the waiver stops suppressing the finding and the posture additionally
raises `deployment.waiver_expired`.
The `reference` is 1 to 128 bytes, has no surrounding whitespace, uses only letters, digits, `.`,
`_`, `:`, and `-`, and cannot contain `..`.
References cannot start, case-insensitively, with `Bearer:<value>` or `Basic:<value>`, directly or
after `Authorization:`.
Use a ticket-style reference such as `OPS-2026-0042`.
The optional `summary` is 1 to 256 Unicode characters when present, is already trimmed, contains
no control characters, and cannot be an authorization value or contain a private-key begin
marker.
Omit `summary` when it is not needed; explicit `null` is invalid.
Keep credentials and private keys out of both fields.
These rules implement
[RS-OP-POSTURE](https://docs.registrystack.org/spec/rs-op-posture/) (REQ-OP-POSTURE-011).
A waiver naming a hard gate (`startup_fail` or `readiness_fail` severity under the active profile)
fails config load instead of being silently accepted and ignored: there is no config-level
override for a non-waivable gate.

Waiver references and summaries are visible only in the restricted posture tier; the default tier reports finding id, severity, and status without the per-finding waiver object or deployment waivers array.

### Findings catalog

| Finding id | hosted_lab | production | evidence_grade |
| --- | --- | --- | --- |
| `relay.admin.public_exposure` | error | readiness_fail | startup_fail |
| `relay.openapi.public` | warn | error | error |
| `relay.ingress.rate_limit_missing` | warn | error | error |
| `relay.oidc.client_allowlist_empty` | warn | error | readiness_fail |
| `relay.auth.api_key_no_rotation_evidence` | warn | error | error |
| `relay.config.unsigned` | warn | error | startup_fail |
| `relay.audit.best_effort` | (not bound) | warn | readiness_fail |
| `relay.audit.sink_missing` | error | readiness_fail | startup_fail |
| `relay.audit.retention_local_only` | (not bound) | warn | startup_fail |
| `relay.audit.shipping_unverified` | (not bound) | warn | startup_fail |
| `relay.audit.shipping_stale` | (not bound) | error | readiness_fail |

`relay.audit.retention_local_only` fires when the audit sink is a local rotating `file` sink and `evidence.audit_offhost_shipping` is not declared: a local rotating file caps retention, and an attacker with host access can destroy the audit trail. `stdout` sinks are exempt (retention is the orchestrator's log pipeline's concern) and `syslog` sinks are exempt (forwarding is the syslog daemon's own surface).

`relay.audit.shipping_unverified` and `relay.audit.shipping_stale` read the ack cursor's observed health. `shipping_unverified` fires when any shipping target (`stdout`, `syslog`, or an attested local `file` sink) lacks `evidence.audit_ack_cursor_path`. It warns under `production` and refuses startup under `evidence_grade`, because a missing observation capability cannot heal at runtime. `shipping_stale` fires when a cursor is configured but is missing, unreadable, malformed, too old, too slow to read, or names a `last_acked_hash` other than the live keyed audit-chain tail. It fails readiness under `evidence_grade` and recovers when the trusted shipper advances a fresh cursor to the current tail. Neither hard gate is waivable. Runtime tail equality establishes that the claimed watermark belongs to this chain and the local backlog is zero; the unsigned local cursor is not cryptographic proof of remote receipt. Offline `doctor` cannot bind to a live chain and therefore reports a fresh cursor as `unverified`, never `ok`; an evidence-grade offline check consequently reports the hard shipping gate. The signed-bundle acceptance audit advances the tail before Relay serves requests, so the shipper must run independently of application readiness and acknowledge that boot record before `/ready` can return 200. Remediation: configure the cursor maintained by the off-host shipper, restore shipping, adjust `evidence.audit_ack_max_age_secs` if the cadence is legitimately slower, or repair a path or watermark mismatch. Removing the cursor does not satisfy `evidence_grade`.

The current deployment profile, its findings, and active waivers are reported under `deployment` in the operations posture (`GET /admin/v1/posture`).

### Boot-time visibility

Reduced posture is loud at boot, not only visible on the posture surface. Every config load warns once per waiver-suppressed finding (`deployment.gate_waived`, with the finding id, reference, optional summary, and expiry), once per expired waiver (`deployment.waiver_expired`), and once when the profile is undeclared (`deployment.profile_undeclared`). The serve path additionally writes one operational audit record per waived gate at boot, once the audit pipeline exists: event `deployment.gate_waived` at audit path `/__events/deployment.gate_waived`, with `error_code` set to the gate id. That minimized audit record does not copy waiver metadata.

This boot-time audit write inherits `audit.write_policy` (see below). Under `fail_closed` (the default), a failed write aborts startup. Under `availability_first`, the failure is logged (`audit.operational_event_write_failed`) and startup continues, so the durable record is best-effort; the per-gate boot log warnings above remain the guaranteed floor.

## Datasets

Each dataset combines private storage tables with public entities:

```yaml
datasets:
  - id: social_registry
    title: Social Registry
    description: Registry of households participating in Program X
    owner: Ministry of Social Affairs
    sensitivity: personal
    access_rights: restricted
    update_frequency: monthly
    conforms_to:
      - psc:concepts/Person
    defaults:
      materialization: snapshot
    tables: []
    entities: []
```

`sensitivity`, `access_rights`, and `update_frequency` are catalog metadata. Set them precisely in production configs; governance reviews depend on them. Allowed values:

- `sensitivity`: `public`, `internal`, `personal`, `confidential`, or `secret`.
- `access_rights`: `public`, `restricted`, or `non_public`.
- `update_frequency`: `continuous`, `daily`, `weekly`, `termly`, `monthly`, `quarterly`, `annual`, `irregular`, `as_needed`, or `unknown`.

`defaults` is optional. It may provide `materialization` and `refresh` defaults for tables in the same dataset. Source configuration stays table-level.

### Sources

Sources are configured on each private table. File sources read CSV, XLSX, or Parquet data:

```yaml
source:
  type: file
  path: ./data/social_registry.xlsx
  format:
    xlsx:
      sheet: Individuals
      header_row: 1
      data_range: A1:E100000
```

For CSV files, set `format.csv.header_row: 1` when the first row contains column names. For XLSX files, `header_row` and `data_range` can be used when a worksheet has notes or title rows around the rectangular table. Source configuration is table-local: put file/database settings and format hints under each `tables[].source`.

Postgres snapshot sources are supported. Credentials are never stored in YAML:

```yaml
source:
  type: postgres
  connection_env: SOCIAL_REGISTRY_DATABASE_URL
  table:
    schema: public
    name: individuals
  change_token_sql: "select max(updated_at)::text from public.individuals"
```

`connection_env` is the environment variable name containing the connection string. Validation and logs may mention the env var name but must not read or print its value. The connection string must set `sslmode=require`; missing `sslmode`, `sslmode=prefer`, and `sslmode=disable` are rejected when the connector reads the environment variable. The native TLS connector validates the server certificate and hostname against the system trust store. Use read-only database credentials. Registry Relay opens read-only Postgres sessions during controlled ingest and refresh, and credentials must enforce the same boundary at the database. `table` and `query` are mutually exclusive; prefer structured `table` configs for production.

Snapshot ingest reads Postgres through `COPY (SELECT ...) TO STDOUT WITH CSV HEADER`, then applies the same declared-schema coercion and validation as CSV files. The exported snapshot is bounded by `server.max_source_file_bytes`. For `table` sources, Registry Relay projects the declared schema fields from the table and casts them to CSV-friendly values. Extra database columns are ignored. For `query` sources, write a single `SELECT` or `WITH` statement without semicolons; public request input is never interpolated into SQL.

The connection string must include `sslmode=require` and point to a read-only database role that can `SELECT` only the configured table or view. Declared schema fields are the exported contract. Public queries run against the ingested snapshot and never cause request-time access to the configured Postgres source.

Minimal source-only form:

```yaml
source:
  type: postgres
  connection_env: SOCIAL_REGISTRY_DATABASE_URL
  table:
    schema: public
    name: individuals
  connect_timeout: 5s
  query_timeout: 30s
```

Supported Postgres field mappings are:

```text
string -> text
integer -> bigint
number -> double precision
boolean -> boolean
date -> date
timestamp -> timestamptz rendered as RFC 3339 UTC text
```

### Refresh

```yaml
refresh:
  mode: mtime
  interval: 60s
```

```yaml
refresh:
  mode: interval
  interval: 1h
```

```yaml
refresh:
  mode: manual
```

`mtime` reloads when the source change token changes. It is supported for file sources and for Postgres snapshot sources only when `change_token_sql` is configured. `interval` reloads on every interval. `manual` reloads only through the admin listener's table reload route.

## Tables

Tables are private storage resources. Their ids do not appear in public URLs.

```yaml
tables:
  - id: individuals_table
    materialization: snapshot
    source:
      type: file
      path: ./data/social_registry.xlsx
      format:
        xlsx:
          sheet: Individuals
    refresh:
      mode: mtime
      interval: 1h
    primary_key: individual_id
    schema:
      strict: true
      fields:
        - name: individual_id
          type: string
          nullable: false
        - name: payment_amount
          type: number
          nullable: true
          unit: EUR
```

Supported formats are `csv`, `xlsx`, and `parquet`. If `format` is omitted, the loader infers from the source file extension where possible.

`materialization` may be `snapshot`. File and Postgres sources are ingested into snapshots.

### Datasource capability matrix

Registry Relay derives datasource capabilities from `source.type` and `materialization`. Operators do not configure these flags directly.

| Source | Materialization | Filters | Projection | Limit | Validators and cursors | Provenance |
| --- | --- | --- | --- | --- | --- | --- |
| `file` | `snapshot` | gateway-side | gateway-side | gateway-side | strong snapshot tokens | snapshot-backed |
| `postgres` `table` or `query` | `snapshot` | gateway-side | gateway-side | gateway-side | strong snapshot tokens | snapshot-backed |

`materialization: live` is rejected at config parse time. Postgres `table` and `query` sources are snapshot-only, so operator SQL is executed only during controlled ingest or refresh and never per public request. Future request-time source access requires a request-aware backend with explicit policy enforcement and bounded execution.

At startup, Registry Relay logs one `ingest.datasource_capabilities` event per configured table.

Field types:

```text
string, number, integer, boolean, date, timestamp
```

Use `sensitive: true` on source or entity fields whose query values need audit
correlation without raw value storage. With `audit.hash_secret_env` configured,
Registry Relay writes a deterministic `hmac-sha256:<digest>` audit handle for
those lookup values. As of v0.8, this flag is audit-only: it does not hide a field
from API responses and does not grant or deny read access. Choose it for
identifiers, names, dates of birth, addresses, consent references, and other
values you may need to investigate later without retaining the raw value in
audit logs.

## Entities

Entities are the public REST resources:

```yaml
entities:
  - name: individual
    title: Individual
    description: A person enrolled in Program X
    table: individuals_table
    concept_uri: psc:concepts/Person
    fields:
      - name: id
        from: individual_id
        sensitive: true
      - name: payment_amount
        from: payment_amount
    relationships:
      - name: household
        kind: belongs_to
        target: household
        foreign_key: household_id
    access:
      metadata_scope: social_registry:metadata
      aggregate_scope: social_registry:aggregate
      read_scope: social_registry:rows
      evidence_verification_scope: social_registry:evidence_verification
    api:
      default_limit: 100
      max_limit: 1000
      require_purpose_header: true
      required_filters:
        - id
      allowed_filters:
        - field: id
          ops: [eq, in]
      allowed_expansions:
        - household
```

When `fields` is present, only listed fields are exposed. When it is omitted, every table column is exposed. For sensitive datasets, prefer an explicit field list. Use entity `read_scope`, required filters, purpose-header requirements, and explicit field projection for exposure control; `sensitive: true` controls audit redaction only.

`required_filters` is an OR gate, not an AND gate: a principal-bound equality filter on any listed field satisfies the requirement. Use `required_filter_bindings` for the principal-derived fields that may satisfy the gate, and list multiple `required_filters` only when each field is an acceptable row boundary.

Row-level authorization scopes are not supported. The `row_scope` resource setting is rejected by config parsing; model row exposure with dataset/entity read scopes, required filters, purpose headers, and projected fields instead.

Relationships are dataset-local in V1. Cross-dataset workflows must compose client-side with separate scoped calls and separate audit records.

### OGC API features

Build with `--features ogcapi-features` to expose spatial entities through the protected `/ogc/v1` surface. The feature does not add a top-level `standards` config block. Instead, opt in per entity with `spatial`:

```yaml
spatial:
  collection_id: facilities
  title: Public facilities
  description: Public facility locations from the civic registry.
  geometry:
    kind: point
    longitude_field: lon
    latitude_field: lat
    crs: http://www.opengis.net/def/crs/OGC/1.3/CRS84
  datetime_field: updated_at
  max_bbox_degrees: 5.0
  max_geometry_vertices: 10000
```

V1 supports `kind: point` and `kind: geojson`. Point longitude, point latitude, datetime, and bbox helper fields must be exposed entity fields with compatible types. `kind: geojson` may use optional precomputed bbox fields:

```yaml
spatial:
  collection_id: parcels
  geometry:
    kind: geojson
    field: geometry
    crs: http://www.opengis.net/def/crs/OGC/1.3/CRS84
  bbox_fields:
    min_x: bbox_min_x
    min_y: bbox_min_y
    max_x: bbox_max_x
    max_y: bbox_max_y
```

Only CRS84 is accepted. `wkt` and `wkb` parse as reserved geometry kinds but are rejected by V1 validation. Collection ids default to the entity name and must be unique within a dataset. OGC discovery uses metadata scope; feature item reads use `read_scope` and preserve entity required filters, purpose-header requirements, projection, and audit behavior.

### Evidence verification

Evidence offerings expose Registry Notary discovery metadata:

```http
GET /metadata/evidence-offerings
GET /metadata/evidence-offerings/{offering_id}
```

Relay's evidence-offering routes do not verify claims or evidence.
`registry-notary` is the verifier for those offerings. The portable metadata
manifest declares public offerings with `access.kind: registry-notary`,
`endpoint_url`, `discovery_url`, and `ruleset` so clients can discover the
Notary service that owns verification. This handoff is independent of Relay's
native, profile-bound source consultation API.

```yaml
access:
  evidence_verification_scope: social_registry:evidence_verification
```

`evidence_verification_scope` remains a scope label for standards adapters and integrations that need to distinguish evidence-oriented access from row reads. It does not enable a Relay-local verification endpoint.

## Aggregates

Aggregates are declared on datasets and name their source entity:

```yaml
aggregates:
  - id: by_municipality
    title: Individuals by municipality
    description: Number of individuals by municipality
    source_entity: individual
    default_group_by:
      - municipality_code
    dimensions:
      - id: municipality_code
        label: Municipality
        field: municipality_code
    indicators:
      - id: individual_count
        label: Individuals
        function: count
        column: id
        unit_measure: people
    allowed_filters:
      - field: municipality_code
        ops: [eq, in]
      - field: enrolled_on
        ops: [gte, lte, between]
    temporal_field: enrolled_on
    disclosure_control:
      min_group_size: 5
      suppression: omit
```

Supported aggregate functions include the configured V1 set used by tests and examples, such as `count`, `sum`, and `avg`. The runtime config key remains `indicators` for compatibility; public aggregate APIs expose these configured series as measures. `temporal_field` is optional; when present, native aggregate `temporal.from` and `temporal.to` are translated into the declared range-capable allowed filter for that source-entity field. Dataset measure and dimension discovery is derived from these aggregate declarations, so keep ids stable and labels consumer-friendly. Keep disclosure thresholds explicit and reviewable.

### Spatial EDR aggregates

Spatial EDR exposure is opt-in. Requires `--features ogcapi-edr`.

```yaml
aggregates:
  - id: by_admin_area
    description: Individuals by administrative area
    source_entity: individual
    # ...dimensions, indicators, disclosure_control as normal...
    spatial:
      mode: admin_area
      collection_id: by_admin_area   # optional; defaults to aggregate id
      dimension: municipality_code   # declared dimension id used to join geometry
      geometry_entity: municipality  # entity name that holds geometry rows
      geometry_id_field: code        # field in geometry_entity matching the dimension values
      geometry_field: geometry       # geojson field in geometry_entity
      bbox_fields:                   # optional precomputed bbox fields in geometry_entity
        min_x: bbox_min_x
        min_y: bbox_min_y
        max_x: bbox_max_x
        max_y: bbox_max_y
      max_geometry_vertices: 10000   # optional; defaults to 10000
```

| Field | Default | Notes |
| --- | --- | --- |
| `mode` | (required) | Must be `admin_area` |
| `collection_id` | aggregate id | OGC collection identifier; must be unique within the dataset |
| `dimension` | (required) | Declared aggregate dimension id whose values are joined to geometry |
| `geometry_entity` | (required) | Entity that holds one geometry row per dimension value |
| `geometry_id_field` | (required) | Field in `geometry_entity` that matches dimension values |
| `geometry_field` | (required) | GeoJSON geometry field in `geometry_entity` |
| `bbox_fields` | absent | Optional precomputed bbox columns; same subkeys as entity `spatial.bbox_fields` |
| `max_geometry_vertices` | 10000 | Cap on GeoJSON vertices decoded from `geometry_field` |

`geometry_entity` must be an entity declared in the same dataset. `geometry_id_field` and `geometry_field` must be exposed entity fields with compatible types (string/integer for id, geojson-typed string for geometry). Only `kind: geojson` geometry is supported for spatial aggregates in V1.

## Credential issuance

Relay no longer accepts `provenance` or entity `publicschema` config. Remove those blocks, Relay signer environment variables, and probes for `/.well-known/did.json`, `/schemas/{claim_type}/{version}`, and `/contexts/{vocab}/{version}` before upgrading.

Use Registry Notary for credential issuance and verification. Relay metadata can advertise Notary evidence offerings with `access.kind: registry-notary`; see [provenance.md](../provenance/) for the migration note.

## Production checklist

- Source files are read-only to the process.
- `cache_dir` is writable and on a filesystem with enough space.
- Every env-backed `fingerprint.name` exists in the runtime environment.
- No raw key, fingerprint, private JWK, or full environment dump is logged.
- Admin listener, if enabled, is private.
- CORS origins are explicit.
- Personal-data entities use explicit field projections.
- Row and evidence-verification routes that need purpose tracking set `require_purpose_header: true`.
- Sensitive identifier fields are marked `sensitive: true` where audit redaction is required.
- Audit sink and retention match the deployment's governance requirements.
- Postgres credentials use a read-only role limited to configured tables or views.