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

# See it live

> With zero install, read a protected registry API from your terminal and receive a privacy-preserving credential in a hosted demo wallet, all against the public hosted lab.

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](https://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

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](https://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](https://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](https://dhis2-notary.lab.registrystack.org).

For how these connect to the rest of the stack, see the
[architecture overview](../../explanation/architecture/). For the full set of live demo
credentials, identities, and ready-made requests, open the lab homepage at
[lab.registrystack.org](https://lab.registrystack.org).

:::note
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](https://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](../../tutorials/first-run-with-registry-lab/).

## 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](https://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.

```sh
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)

The metadata-scope token can list the datasets the Relay publishes:

```sh
curl -sS \
  -H "Authorization: Bearer $CIVIL_METADATA_TOKEN" \
  https://civil-relay.lab.registrystack.org/v1/datasets
```

The Relay returns `200 OK` with the catalog. Each dataset entry carries more fields than shown;
they are omitted here for brevity:

```json
{ "data": [ { "dataset_id": "civil_registry" } ] }
```

### 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:

```sh
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):

```json
{
  "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

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:

```sh
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](../../products/registry-relay/).

## Get a signed credential

Now see the issuance side. The citizen Notary issues a Selective Disclosure JWT Verifiable
Credential ([SD-JWT VC](../../reference/glossary/#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](../../reference/glossary/#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](https://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](https://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:

```text
https://citizen-notary.lab.registrystack.org/oid4vci/credential-offer?credential_configuration_id=person_is_alive_sd_jwt
```

### 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](../../products/registry-notary/).

### Developer API round-trip

For a pure API credential flow, use the hosted DHIS2 Notary: the lab runs a demo
[DHIS2](https://dhis2.org/) 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.)

```sh
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:

```sh
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_BEARER
```

Evaluate the credential claims for the live demo tracked entity:

```sh
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.json
```

Issue the SD-JWT VC from that evaluation:

```sh
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.json
```

Fetch the issuer keys and verify the credential.
The JWKS endpoint is public: any verifier can fetch the issuer keys without a credential.

```sh
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));
NODE
```

Expected verification summary:

```json
{
  "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

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](../../tutorials/publish-spreadsheet-secured-registry-api/):
  stand up your own protected Relay from a sample workbook.
- [Verify a claim with Registry Notary](../../tutorials/verify-claim-registry-api/): 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](../../tutorials/verify-claim-registry-api/#run-notary-standalone-for-an-api-you-operate).

Only assessing fit? You do not need to install anything:
[When to use Registry Stack](../when-to-use/) covers fit and non-goals, and
[DPI safeguards alignment](../../explanation/dpi-safeguards-alignment/) maps the stack to
safeguards language for review programs.