Skip to content
Registry StackDocsLatest

First run with Solmara Lab

View as Markdown

Solmara Lab is a standalone Registry Stack adopter demo for the fictional Republic of Solmara: six Registry Relay instances and four Registry Notary instances wired over generated synthetic data into one topology, alongside a citizen portal and a public Visitor’s Center. This tutorial brings the topology up with the lab’s just recipes, verifies it with the smoke suite, and calls a Notary evaluation route directly so you touch the protected API surface, not only the portal.

Outcome
The full Solmara Lab topology running on your laptop, verified by the smoke suite, with a Notary evaluation call made directly against the running stack.
Time
About 20 minutes, most of it the first startup (image pulls plus three local image builds)
Level
Local multi-service demo
Prerequisites
Docker Composejustuvpython3 and opensslpnpm 11.3.0 via corepackgitcurl

This tutorial uses synthetic data and local demo credentials. Do not use the generated local keys in production.

If you only want the shortest hosted path before running anything locally, start with the Quickstart instead. For a standalone local project generated from your own data, use the registryctl tutorials: run a protected registry API or evaluate a claim with Registry Notary. Clone Solmara Lab when you want the full multi-service country demo.

  • Docker with Compose v2. just up and just down run docker compose ... up -d --build and docker compose ... down.
  • The just command runner. Every step in this tutorial is a just recipe.
  • uv. just generate runs the fixture generator with uv run python -m solmara_lab.generate.
  • python3 and openssl on your PATH. just generate also runs scripts/gen-secrets.py directly, a plain Python script that shells out to openssl for local TLS and signing key material.
  • pnpm 11.3.0, matching the packageManager field the portal and Visitor’s Center pin. Run corepack enable first if pnpm is not already on your PATH.
  • git.
  • curl, for the direct Notary evaluation call and the verify commands.

Solmara Lab is a standalone repository, not a directory inside the Registry Stack monorepo. Clone it and install its dependencies:

Terminal window
git clone https://github.com/registrystack/solmara-lab
cd solmara-lab
just setup

just setup installs the root and generator Python environments with uv sync, and installs the portal and home (Visitor’s Center) Node dependencies with pnpm install --frozen-lockfile.

Run these from your Solmara Lab checkout, in order.

  1. Generate the deterministic fixtures, local demo secrets, and Postgres TLS material:

    Terminal window
    just generate
  2. Build the local images and start the topology in the background:

    Terminal window
    just up
  3. Run the smoke suite, the primary verification:

    Terminal window
    just smoke

just generate also writes .env, a Compose-required file of local demo credentials. Skipping it is the most common first failure; see the troubleshooting table. just up builds the portal, home, and scenario-runner images locally and pulls the digest-pinned Relay and Notary images; expect the first run to take most of this tutorial’s time. just smoke runs story-preview checks plus, by default, live checks against the running Relay, Notary, and portal services.

just up starts 16 long-running services: six purpose-scoped Relay instances, one per Solmara ministry authority, four purpose-scoped Notary instances, the citizen portal, the public Visitor’s Center, a static metadata publisher, a scenario-runner service that backs the portal’s guided demos, Postgres, and Redis.

Solmara Lab compose topology, 16 services. Six authority Relay instances
(cra-civil-relay :4311, nia-population-relay :4312, sro-social-relay :4313,
programme-mis-relay :4314, sipf-pensions-relay :4315, nagdi-agriculture-relay :4316)
provide evidence reads to four Notary instances (child-benefit-notary :4321, which
you call directly in this tutorial; pension-notary :4322; nagdi-notary :4323;
citizen-notary :4324). The citizen portal :4300 is wired to the citizen Notary;
the Visitor's Center runs on :4301. A support strip holds static-metadata :4331,
scenario-runner, Postgres :54329, and Redis :63799.

The services this tutorial calls directly:

ServiceHost portRole
portal4300Citizen portal: a SvelteKit application wired to the citizen Notary.
home4301Visitor’s Center: the lab’s public tour, including an Engineer door.
child-benefit-notary4321Notary for the birth-to-child-benefit journey; you call its evaluation route directly.
static-metadata4331Unauthenticated multi-authority metadata bundle.

The other six Relay instances, three Notary instances, Postgres, and Redis back the pension and farmer-voucher journeys and the portal’s own evidence calls; this tutorial does not call them directly. The topology figure shows the full port map.

Each instance publishes its rendered API reference at /docs, a route that is always public, and its raw /openapi.json, public here because every Relay and Notary configuration in this lab sets openapi_requires_auth: false. This is the same route pattern documented in Evaluate a claim with Registry Notary.

See it: the citizen portal and the Visitor’s Center

Section titled “See it: the citizen portal and the Visitor’s Center”

Open the citizen portal at http://127.0.0.1:4300. It is a SvelteKit application backed by the citizen Notary and the civil, social, and agriculture Relay instances. Sign in with one of the portal’s fixed demo personas to see a resident’s own view of the evidence flows this tutorial calls directly.

Open the Visitor’s Center at http://127.0.0.1:4301. It is Solmara Lab’s public tour, organized as three doors: visit as a citizen, as a relying agency running a guided story, or as an engineer. Scroll to the Engineer door section. It republishes the same four synthetic Notary tokens this tutorial’s .env generates: child-benefit-notary, pension-notary, nagdi-notary, and citizen-notary. They appear as ready-made copy-as-curl examples, including the evaluation call you make by hand next.

The birth-to-child-benefit journey is one of the lab’s guided stories. Its positive fixture is Mateo Santos, Solmara UIN 2300010248: a child whose birth is registered, who is under five, whose household falls below the poverty threshold, and who is not already enrolled.

Load the generated Notary token for this journey from .env, then evaluate the same four claims the birth-to-child-benefit story checks:

Terminal window
export CHILD_BENEFIT_NOTARY_TOKEN="$(grep -m1 '^CHILD_BENEFIT_NOTARY_TOKEN=' .env | cut -d= -f2-)"
Terminal window
curl -sS -X POST \
-H "x-api-key: $CHILD_BENEFIT_NOTARY_TOKEN" \
-H "Data-Purpose: https://id.registrystack.org/solmara/purpose/child-benefit-review" \
-H "Content-Type: application/json" \
-H "Accept: application/vnd.registry-notary.claim-result+json" \
-d '{
"target": {"type": "Person", "identifiers": [{"scheme": "solmara_uin", "value": "2300010248"}]},
"claims": ["birth-is-registered", "child-age-under-5", "household-below-poverty-threshold", "not-already-enrolled"],
"disclosure": "predicate",
"format": "application/vnd.registry-notary.claim-result+json"
}' \
http://127.0.0.1:4321/v1/evaluations

The Notary returns 200 OK with all four claims satisfied. The important fields look like this; other fields are omitted:

{
"results": [
{ "claim_id": "birth-is-registered", "satisfied": true, "value": true },
{ "claim_id": "child-age-under-5", "satisfied": true, "value": true },
{ "claim_id": "household-below-poverty-threshold", "satisfied": true, "value": true },
{ "claim_id": "not-already-enrolled", "satisfied": true, "value": true }
]
}

The x-api-key header carries the caller’s credential. Data-Purpose declares why the caller is asking, and the Notary’s configuration allows this token to ask only under the child-benefit-review purpose. The disclosure: predicate request means the response discloses a true or false satisfaction per claim, never the underlying civil, population, social, or programme registry row the Notary read to answer it. The Notary can also issue a signed SD-JWT verifiable credential (SD-JWT VC) for this same eligibility claim set; that holder-bound issuance flow is out of scope for this tutorial. See Registry Notary for the credential issuance contract.

just smoke (step 3) is the authoritative check. These commands confirm its artifacts are on disk, and reproduce the call from Call a Notary evaluation route directly in a single command so it can run without a shell variable.

Terminal window
ls output/smoke/story-previews.json
Terminal window
ls output/smoke/relay-sources.json
Terminal window
ls output/smoke/portal-compose.json
Terminal window
curl -sS -X POST -H "x-api-key: $(grep -m1 '^CHILD_BENEFIT_NOTARY_TOKEN=' .env | cut -d= -f2-)" -H "Data-Purpose: https://id.registrystack.org/solmara/purpose/child-benefit-review" -H "Content-Type: application/json" -H "Accept: application/vnd.registry-notary.claim-result+json" -d '{"target":{"type":"Person","identifiers":[{"scheme":"solmara_uin","value":"2300010248"}]},"claims":["birth-is-registered","child-age-under-5","household-below-poverty-threshold","not-already-enrolled"],"disclosure":"predicate","format":"application/vnd.registry-notary.claim-result+json"}' http://127.0.0.1:4321/v1/evaluations | python3 -c "import json, sys; data = json.load(sys.stdin); got = {r['claim_id']: r['satisfied'] for r in data['results']}; expected = ['birth-is-registered', 'child-age-under-5', 'household-below-poverty-threshold', 'not-already-enrolled']; raise SystemExit(0 if len(data['results']) == 4 and all(got.get(c) is True for c in expected) else 1)"

You brought up 16 Relay, Notary, portal, and supporting services on your laptop from a single just up, and the smoke suite verified the topology. You then called the child benefit Notary directly and watched it return four satisfied predicates for Mateo Santos without exposing the civil, population, social, or programme registry rows it read to answer. The same synthetic token and evaluation call are also visible through the Visitor’s Center’s Engineer door, so you can confirm what you ran by hand against what the lab publishes.

SymptomCauseResolution
just up fails immediately with required variable ... is missing a value: .env not loaded; run just gen-secrets once, then just upjust generate (or just gen-secrets) never ran, so .env does not exist.Run just generate, then just up again.
docker compose reports a port already allocatedAnother process on your machine already holds one of the lab’s ports: 4300, 4301, 4311 through 4316, 4321 through 4324, 4331, 54329, or 63799.Stop the conflicting process, or set the matching SOLMARA_*_PORT environment variable (for example SOLMARA_PORTAL_PORT=4400) before just up.
command not found: uv or command not found: pnpmA required tool is not on your PATH.Install uv, or run corepack enable for pnpm, then run just setup again.
docker compose ps -a lists lab containers you did not startThe lab derives a per-checkout Compose project name (solmara-lab- plus a hash of the checkout path), so two checkouts never share a project. Leftover containers come from a lab version that used the shared project name solmara-lab.Inspect the old project with docker compose -p solmara-lab ps, then remove it with docker compose -p solmara-lab down (add -v only if you also want its volumes deleted).
The Relay and Notary containers start slowly or show high CPU on Apple SiliconThe pinned Relay and Notary images publish linux/amd64 only, so Compose runs them under emulation.Expected. Allow extra time on the first just up; later runs are faster once the images are cached.
The first just up appears to hangThe first run pulls the pinned Relay and Notary images and builds the portal, home, and scenario-runner images locally.Let it finish; do not interrupt it.

Stop the containers:

Terminal window
just down

just down runs docker compose ... down, keeping the Postgres data volume and the Relay and Notary state volumes, so a later just up resumes with the same data. To also remove the volumes, run just reset instead; that is the lab’s documented clean slate: rerun just generate and just up for a fresh topology.