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.
In about ten minutes you can see the core of Registry Stack working against a public hosted lab. You
will read a protected registry API from your terminal (Registry Relay, which returns records to
authorized callers), then have Registry Notary issue a signed credential, delivered to a hosted
demo wallet, that answers a question without exposing the record. The Relay reads are pure curl
with zero install; the credential step is a guided browser flow using the lab’s hosted wallet, so it
needs no install either; only the optional developer round-trip at the end also uses jq and
node. Everything runs in the hosted lab at
lab.registrystack.org, so there is no setup on your machine.
This lab uses synthetic data and public demo-only credentials by design. Do not reuse anything you copy here outside the lab.
What is running
Section titled “What is running”The lab runs three services that you will touch directly:
- A civil-registry Relay: a protected, scoped, read-only HTTP API over a civil registry source, at civil-relay.lab.registrystack.org.
- A citizen Notary: an issuer that hands out a privacy-preserving credential instead of the raw record, at citizen-notary.lab.registrystack.org.
- A health-program Notary in front of a demo DHIS2 health information system, used only by the optional developer round-trip at the end of this page, at dhis2-notary.lab.registrystack.org.
For how these connect to the rest of the stack, see the architecture overview. For the full set of live demo credentials, identities, and ready-made requests, open the lab homepage at lab.registrystack.org.
Lab tokens are public demo-only values that rotate, so this page shows placeholders, never live tokens. Get the current tokens and identities from the For developers section near the bottom of the lab homepage, which shows every demo credential with a copy button and a prebuilt curl example. The same data is available unauthenticated as JSON at lab.registrystack.org/api/lab.json.
If the hosted lab or one of its service URLs is unavailable, run the same multi-service topology locally with First run with Registry Lab.
Read a protected registry API
Section titled “Read a protected registry API”This is the part you can run right now from a terminal. The civil Relay authenticates callers with
Authorization: Bearer <token>. Each token carries a scope, so the same API answers some requests
and refuses others.
Open lab.registrystack.org, scroll to the For developers section near the bottom, and copy two bearer tokens from the demo credential cards: a metadata-scope token and a row-reader token. Set them in your shell. The placeholders below stand in for the live tokens.
export CIVIL_METADATA_TOKEN="<metadata token from lab.registrystack.org>"export CIVIL_ROW_READER_TOKEN="<row-reader token from lab.registrystack.org>"List datasets (metadata scope)
Section titled “List datasets (metadata scope)”The metadata-scope token can list the datasets the Relay publishes:
curl -sS \ -H "Authorization: Bearer $CIVIL_METADATA_TOKEN" \ https://civil-relay.lab.registrystack.org/v1/datasetsThe Relay returns 200 OK with the catalog. Each dataset entry carries more fields than shown;
they are omitted here for brevity:
{ "data": [ { "dataset_id": "civil_registry" } ] }Read rows (row scope plus a purpose)
Section titled “Read rows (row scope plus a purpose)”Reading actual records needs a token with the row scope and a Data-Purpose header that declares why
you are reading:
curl -sS \ -H "Authorization: Bearer $CIVIL_ROW_READER_TOKEN" \ -H "Data-Purpose: https://demo.example.gov/purpose/decentralized-evidence-demo" \ "https://civil-relay.lab.registrystack.org/v1/datasets/civil_registry/entities/civil_person/records?limit=1"The Relay returns 200 OK with one record and pagination metadata (synthetic demo data):
{ "data": [ { "national_id": "NID-1001", "given_name": "Miguel", "surname": "Santos", "birth_date": "2016-01-15", "life_stage": "child", "deceased": false, "district": "north" } ], "pagination": { "has_more": true, "next_cursor": "..." }}See access control hold
Section titled “See access control hold”The point of the gateway is that it never widens reach at request time. Send the metadata-scope token to the rows endpoint and the Relay refuses it:
curl -sS -o /dev/null -w "%{http_code}\n" \ -H "Authorization: Bearer $CIVIL_METADATA_TOKEN" \ -H "Data-Purpose: https://demo.example.gov/purpose/decentralized-evidence-demo" \ "https://civil-relay.lab.registrystack.org/v1/datasets/civil_registry/entities/civil_person/records?limit=1"The Relay returns 403 Forbidden with {"code":"auth.scope_denied"}. Same API, same network call,
different scope, different answer. For more on this service, see
Registry Relay.
Get a signed credential
Section titled “Get a signed credential”Now see the issuance side. The citizen Notary issues a Selective Disclosure JWT Verifiable
Credential (SD-JWT VC) of type person_is_alive_sd_jwt,
media type application/dc+sd-jwt. The credential discloses a single predicate, whether
the person is alive, as a true or false claim. It never hands over the underlying civil record.
The Relay row-read example uses NID-1001 / Miguel Santos, while this wallet flow uses the lab
wallet identity NID-2001 / Maria Santos.
Unlike the Relay reads above, this part is a guided browser flow, not a curl sequence: a real OID4VCI issuance needs a holder key and a sign-in, which is wallet territory. You do not need to install a wallet: the lab hosts a demo wallet at wallet.lab.registrystack.org, so you can receive the credential end to end in the browser.
Start in the Wallet test section of lab.registrystack.org, which links the credential flow end to end: start the citizen Notary flow, sign in, then paste the generated credential offer into the hosted wallet. The Notary requires you to sign in before it issues. The sign-in runs through eSignet, the lab’s hosted demo identity provider; you do not need an account, and the values below are synthetic demo identities:
- National ID:
NID-2001 - Name: Maria Santos
- Login and OTP code:
111111 - PIN:
545411
After sign-in, the hosted wallet receives the signed person_is_alive_sd_jwt credential, with the
alive predicate set to true. If you prefer your own OID4VCI-compatible wallet, you can open the
credential offer directly instead:
https://citizen-notary.lab.registrystack.org/oid4vci/credential-offer?credential_configuration_id=person_is_alive_sd_jwtThe negative control
Section titled “The negative control”Try this in the same browser flow. Self-attestation binds the credential subject to the identity you
signed in as. If you sign in as NID-2001 but ask for a credential bound to NID-1001, the Notary
refuses: it will not issue a credential for a subject you did not authenticate as. The issuer cannot
be talked into vouching for someone else. For more on this service, see
Registry Notary.
Developer API round-trip
Section titled “Developer API round-trip”For a pure API credential flow, use the hosted DHIS2 Notary: the lab runs a demo
DHIS2 health information system with a Notary in front of it, so the same
issuance contract can be exercised against a second, independent source system. This path does
not use the browser wallet sign-in, and unlike the curl-only reads above it also needs jq and
node on your machine. It evaluates DHIS2 child-program claims, issues an application/dc+sd-jwt
credential, fetches the issuer JWKS, verifies the Ed25519 signature, and checks that each disclosure
hash is listed in the issuer-signed JWT.
Set the hosted Notary URL and load the current demo bearer token from the lab JSON. (lab.json
carries a credentials array whose entries have an id and a token, plus the service list; the
dhis2-bearer entry is the demo evidence-client token for this Notary.)
export DHIS2_NOTARY_URL="https://dhis2-notary.lab.registrystack.org"export DHIS2_PURPOSE="https://demo.example.gov/purpose/dhis2-openfn-health-evidence"export DHIS2_EVIDENCE_CLIENT_BEARER="$( curl -fsS https://lab.registrystack.org/api/lab.json | jq -r '.credentials[] | select(.id == "dhis2-bearer") | .token')"If you already have registryctl, you can load the same values from the
hosted lab manifest through the helper:
eval "$( registryctl lab env --credential dhis2-bearer --format json | jq -er ' if ([.base_url, .purpose, .token] | all(type == "string" and length > 0)) then @sh "DHIS2_NOTARY_URL=\(.base_url) DHIS2_PURPOSE=\(.purpose) DHIS2_EVIDENCE_CLIENT_BEARER=\(.token)" else error("registryctl lab env output is missing a required field") end ')"export DHIS2_NOTARY_URL DHIS2_PURPOSE DHIS2_EVIDENCE_CLIENT_BEAREREvaluate the credential claims for the live demo tracked entity:
cat > evaluation-request.json <<'JSON'{ "target": { "type": "TrackedEntity", "identifiers": [ { "scheme": "dhis2_tracked_entity", "value": "PQfMcpmXeFE" } ] }, "claims": [ "dhis2-tracked-entity-first-name", "dhis2-tracked-entity-last-name", "dhis2-child-program-active" ], "disclosure": "value", "format": "application/dc+sd-jwt"}JSON
curl -fsS -X POST "$DHIS2_NOTARY_URL/v1/evaluations" \ -H "Authorization: Bearer $DHIS2_EVIDENCE_CLIENT_BEARER" \ -H "Content-Type: application/json" \ -H "Data-Purpose: $DHIS2_PURPOSE" \ --data @evaluation-request.json \ -o evaluation.json
jq '.results[] | {claim_id, value, evaluation_id, format}' evaluation.jsonIssue the SD-JWT VC from that evaluation:
EVALUATION_ID="$(jq -r '.results[0].evaluation_id' evaluation.json)"
jq -n --arg evaluation_id "$EVALUATION_ID" '{ evaluation_id: $evaluation_id, credential_profile: "dhis2_child_program_sd_jwt", format: "application/dc+sd-jwt", claims: [ "dhis2-tracked-entity-first-name", "dhis2-tracked-entity-last-name", "dhis2-child-program-active" ], disclosure: "value"}' > credential-request.json
curl -fsS -X POST "$DHIS2_NOTARY_URL/v1/credentials" \ -H "Authorization: Bearer $DHIS2_EVIDENCE_CLIENT_BEARER" \ -H "Content-Type: application/json" \ --data @credential-request.json \ -o credential.json
jq '{credential_id, credential_profile, issuer, format, disclosure_count: (.disclosures | length)}' credential.jsonFetch the issuer keys and verify the credential. The JWKS endpoint is public: any verifier can fetch the issuer keys without a credential.
curl -fsS "$DHIS2_NOTARY_URL/.well-known/evidence/jwks.json" -o jwks.json
node <<'NODE'const { createHash, createPublicKey, verify } = require('node:crypto');const { readFileSync } = require('node:fs');
const credential = JSON.parse(readFileSync('credential.json', 'utf8'));const jwks = JSON.parse(readFileSync('jwks.json', 'utf8'));const [header64, payload64, signature64] = credential.issuer_signed_jwt.split('.');const header = JSON.parse(Buffer.from(header64, 'base64url').toString('utf8'));const payload = JSON.parse(Buffer.from(payload64, 'base64url').toString('utf8'));const keys = Array.isArray(jwks.keys) ? jwks.keys : [];const key = keys.find((candidate) => candidate.kid === header.kid);
if (!key) throw new Error(`Missing JWKS key ${header.kid}`);
const signed = Buffer.from(`${header64}.${payload64}`);const signature = Buffer.from(signature64, 'base64url');const signatureValid = verify(null, signed, createPublicKey({ key, format: 'jwk' }), signature);
if (!signatureValid) throw new Error('Bad issuer signature');
const disclosureDigests = new Set(payload._sd || []);const disclosureClaims = [];
for (const disclosure of credential.disclosures || []) { const digest = createHash('sha256').update(disclosure).digest('base64url'); if (!disclosureDigests.has(digest)) { throw new Error(`Disclosure digest not present in _sd: ${digest}`); } const decoded = JSON.parse(Buffer.from(disclosure, 'base64url').toString('utf8')); disclosureClaims.push(decoded[1]);}
console.log(JSON.stringify({ issuer: payload.iss, vct: payload.vct, signature_valid: signatureValid, disclosure_count: disclosureClaims.length, disclosure_claims: disclosureClaims.sort()}, null, 2));NODEExpected verification summary:
{ "issuer": "did:web:dhis2-notary.lab.registrystack.org", "vct": "https://dhis2-notary.lab.registrystack.org/credentials/dhis2/child-program/v1", "signature_valid": true, "disclosure_count": 3, "disclosure_claims": [ "dhis2-child-program-active", "dhis2-tracked-entity-first-name", "dhis2-tracked-entity-last-name" ]}Now run your own
Section titled “Now run your own”You have seen the two payoffs against the hosted lab. To run the same shapes on your own machine, start with the local single-node tutorials:
- Publish a spreadsheet as a secured registry API: stand up your own protected Relay from a sample workbook.
- Verify a claim with Registry Notary: run a Notary against the Relay you published and evaluate a claim. Already have your own API? The same tutorial ends with a standalone Notary path.
Only assessing fit? You do not need to install anything: When to use Registry Stack covers fit and non-goals, and DPI safeguards alignment maps the stack to safeguards language for review programs.