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.
Demo flow walkthrough
This page walks you through the three narrated scenarios in
scripts/demo-flow.py
after the Registry Lab compose topology is running.
The focus is on what each step exercises: which service receives the request,
what the request demonstrates, and what you can inspect in logs or output artifacts.
For starting the topology from the beginning, see the First run with Registry Lab tutorial.
When to use this
Section titled “When to use this”Use this walkthrough when the core topology is already up (services started with
docker compose -f compose.yaml up -d) and you want to trace what the demo client does,
understand how scope enforcement works across services, or verify that a change
to a Relay or Notary configuration changes the client output in the expected way.
Prerequisites
Section titled “Prerequisites”- All background services started and healthy. Verify with
scripts/smoke.shbefore running the client. output/directory writable from the compose host. The demo client mounts it at/demo/output.opensslon the host PATH (used bydemo-flow.pyfor Ed25519 holder proof signing when run locally; the compose-based client uses the Python image’sopenssl).- Generated
.envpresent (fromscripts/generate-demo-secrets.py).
Running the demo client
Section titled “Running the demo client”Start the demo client as a one-shot container in the client profile:
docker compose -f compose.yaml --profile client run --rm demo-clientThe client connects to services via their compose DNS names (for example,
http://civil-registry-relay:8080),
runs all three scenarios in order, and exits.
Artifacts land in output/ on the host.
To override the correlation ID used on all requests:
DEMO_CORRELATION_ID=my-trace-id docker compose -f compose.yaml --profile client run --rm demo-clientThe default correlation ID is decentralized-demo-correlation-001.
Every request in the demo carries this value as x-request-id.
Scenario 1: Birth registration to child support
Section titled “Scenario 1: Birth registration to child support”This scenario exercises civil-notary (port 4321 on the host,
http://civil-notary:8080 inside compose).
The demo client:
- Calls
GET /.well-known/evidence-serviceoncivil-notaryto discover the available claims. This endpoint requires theCIVIL_EVIDENCE_CLIENT_BEARERtoken. - Sends
POST /claims/evaluatetocivil-notarywith a subject identifier and a birth-registration claim. The claim uses predicate disclosure: the credential does not return the raw civil registry row, only whether the predicate (born in a registered district, caregiver present) is satisfied. - Optionally sends the same request with
Accept: application/dc+sd-jwtto receive a Selective Disclosure JWT Verifiable Credential (SD-JWT VC) signed with the issuer JWK.
What you’ll see / What this proves for your integration: The Notary fetches facts from the Relay on your behalf, using a configured bearer token. The civil Relay does not write any result back. The Notary applies the configured disclosure profile (predicate) and returns a structured JSON-LD credential body that calling systems can parse for the claim result. CCCEV stands for Core Criterion and Core Evidence Vocabulary.
What to take away: predicate disclosure lets you confirm a fact (born in a registered district, caregiver present) without exposing the underlying registry row to the caller.
What to inspect:
output/scenario1-civil-credential.json: the claim result artifact.- Relay logs:
docker compose logs civil-registry-relay. Look for theevaluateevent and the200response to the Notary source call. - Notary logs:
docker compose logs civil-notary. Look for the evaluation event with the correlation ID.
Scenario 2: Household benefit review from registry data
Section titled “Scenario 2: Household benefit review from registry data”This scenario exercises social-protection-registry-relay (port 4312 on the host,
http://social-protection-registry-relay:8080 inside compose) and scope enforcement.
The demo client:
- Calls
GET /datasets/social_protection_registry/householdon the social protection Relay with theSOCIAL_ROW_READER_RAWtoken and aData-Purposeheader. This is a protected row read: only the row-reader scope allows it. - Calls
GET /datasets/social_protection_registry/household/aggregates/households_by_eligibility_bandon the same Relay with theSOCIAL_AGGREGATE_READER_RAWtoken. This is the aggregate consultation path: only the aggregate-reader scope allows it. - Attempts to call the aggregate endpoint with the row-reader token.
Registry Relay returns a 403 response with
auth.scope_denied. - Writes a household-benefit-decision artifact locally based on the aggregate result.
Registry Relay has no write-back path; the artifact exists only in
output/.
What you’ll see / What this proves for your integration: Scope enforcement is route-level. The row-reader and aggregate-reader credentials have distinct scopes and cannot substitute for each other. All reads are protected; the Relay does not expose any route without a valid credential.
What to take away: you can issue purpose-scoped tokens so that a benefit-decision system can read aggregate bands but cannot reach individual household rows.
What to inspect:
output/scenario2-household-benefit-decision.json: the decision artifact, which includesboundary.relay_write_back = false.- Relay logs:
docker compose logs social-protection-registry-relay. Look forauth.scope_deniedon the rejected aggregate request and the200on the permitted reads.
Scenario 3: Cross-authority conditional support
Section titled “Scenario 3: Cross-authority conditional support”This scenario exercises static-metadata-publisher (port 4331 on the host),
shared-eligibility-notary (port 4323),
and the three Relay instances acting as sources.
The demo client:
- Fetches
GET /metadata/evidence-offerings.jsonfromstatic-metadata-publisher. No authentication is required. The client uses this to discover thatshared-eligibility-notaryhandles cross-authority claims. - Calls
GET /.well-known/evidence-serviceonshared-eligibility-notaryusing theSHARED_EVIDENCE_CLIENT_BEARERtoken. - Sends
POST /claims/evaluatetoshared-eligibility-notarywith a cross-authority claim that requires evidence from civil, social protection, and health registries.
What you’ll see / What this proves for your integration:
The Notary fans out to all three Relay services using per-source bearer tokens configured in
config/notary/shared-eligibility-notary.yaml.
The result carries source_count >= 2 when at least two sources contributed evidence.
What to take away: a single Notary can aggregate evidence across independently operated registries and report how many authorities contributed, without merging or exposing the underlying rows.
What to inspect:
output/scenario3-cross-authority-result.json: the multi-source claim result.- Notary logs:
docker compose logs shared-eligibility-notary. Look for source-fetch events to each Relay and the final evaluation result. - Relay logs for all three services: look for the Notary source-read requests, identifiable by the correlation ID header.
Verification
Section titled “Verification”After the client exits, run the following checks to confirm the artifacts and logs match expectations.
Check that all three scenario artifacts exist:
ls output/Confirm the cross-authority result has at least two sources:
python3 -c "import json,sys; d=json.load(open('output/scenario3-cross-authority-result.json')); sys.exit(0 if d.get('source_count', 0) >= 2 else 1)"Confirm the household benefit decision records no write-back:
python3 -c "import json,sys; d=json.load(open('output/scenario2-household-benefit-decision.json')); b=d.get('boundary',{}); sys.exit(0 if b.get('relay_write_back') is False else 1)"Confirm scope-denial events appear in Relay logs:
docker compose logs social-protection-registry-relay 2>&1 | grep auth.scope_deniedRun the full smoke suite as a post-run gate (it re-runs the demo client internally):
scripts/smoke.shTo exercise the additional live-service stories, run:
scripts/demo-live-stories.shThat script writes Postgres, Zitadel/OIDC, and OpenFn artifacts under output/live-stories/.
It also writes the service-first discovery artifact that shows which Core Criterion and Core
Evidence Vocabulary (CCCEV) evidence option Registry Atlas selected from the published Core Public
Service Vocabulary Application Profile (CPSV-AP) catalogue.
Troubleshooting
Section titled “Troubleshooting”Client exits immediately without producing artifacts.
Confirm all background services are healthy before starting the client.
Run docker compose ps and scripts/smoke.sh. A service not yet healthy causes the client to fail
on its first HTTP request.
403 on evidence discovery (/.well-known/evidence-service).
The bearer token in .env may be stale or mismatched.
Regenerate credentials with scripts/generate-demo-secrets.py and restart the affected
Notary service: docker compose restart civil-notary.
Missing source_count field in scenario 3 output.
The shared eligibility notary received responses from fewer than two Relay sources.
Check that all three Relay services are running and reachable inside the compose network:
docker compose exec shared-eligibility-notary wget -q -O- http://civil-registry-relay:8080/healthopenssl not found when running demo-flow.py directly on the host.
Install openssl or use the compose-based invocation, which uses the Python image’s built-in openssl.
Artifacts from a previous run are overwritten.
demo-flow.py clears output/ on each invocation.
Copy artifacts out before re-running if you need to compare runs.