The stack and the development environment
What each part of the hub is called, what it does and how the parts fit
together, followed by how to run them on a laptop. The decisions behind each
part are in the ADRs linked from its row; the full gotcha log is
docs/gotchas.md.
1. Glossary
1.1 Protocols
| Term | What it is | Decision |
|---|---|---|
| DSP 2025-1 | The Eclipse Dataspace Protocol, version 2025-1: how two connectors exchange a catalogue (DCAT), negotiate a contract on an ODRL offer and run a transfer. The control plane speaks it under /api/dsp/<participant>/2025-1/. |
ADR-007 |
| DCP 1.0 | The Decentralised Claims Protocol: how a participant proves who it is. Each one has a did:web DID and holds verifiable credentials (a MembershipCredential, EHDS role credentials); a counterparty asks for a presentation instead of trusting a central login. |
ADR-009 |
| ODRL 2.2 | The policy language of an offer: who may use a dataset, for what purpose, under which permit. | ADR-013 |
| HealthDCAT-AP | The catalogue vocabulary for health datasets, Release 8 since #700. | ADR-063 |
| did:web | A DID resolved over HTTPS. Two forms coexist, a stable one in the graph and the UI and a resolvable one in the EDC layer; api-conventions.md explains which goes where. |
ADR-007 |
1.2 The connector: EDC-V
| Term | What it is | Decision |
|---|---|---|
| EDC-V | Eclipse Dataspace Components, virtual: one multi-tenant EDC runtime in which every participant is a participant context rather than its own connector. The hub runs EDC 0.18 from the JAD images. | ADR-024, ADR-055 |
| Control plane | The EDC-V runtime that holds assets, policies and contract definitions, negotiates contracts over DSP and starts transfers. Management API on :11003. |
ADR-002 |
| Data planes | Two, one per transfer type: dataplane-fhir pushes FHIR R4 (:11002), dataplane-omop serves OMOP CDM by pull (:11012). They move the data the control plane agreed on and check the EDR token of each request. |
ADR-002 |
| IdentityHub | Each participant's DCP wallet: its DID document, its keys (in Vault) and the credentials it holds; it answers presentation requests. :11005. |
ADR-009 |
| IssuerService | The hub's credential issuer: it holds the credential definitions and issues credentials to holders over DCP. :10013. A second instance, simpl-ga-issuer (:10023), plays the Simpl governance authority; rogue-issuer (:10033) is on no trust list and only starts for the identity checks. |
ADR-009, ADR-064 |
| Siglet | Signs the tokens of the EDC 0.18 data planes (Eclipse Dataplane Core). | ADR-055 |
1.3 Running it: JAD and CFM
| Term | What it is | Decision |
|---|---|---|
| JAD | The Joint Architecture Demo: the reference EDC-V deployment (ghcr.io/metaform/jad/* images, pinned by commit). Here it is docker-compose.jad.yml plus the seeds and configuration in jad/, layered on docker-compose.yml. Azure runs the same build. |
ADR-005, ADR-055 |
| CFM | The Connector Fabric Manager, the management plane above EDC-V: the tenant manager (:11006) creates a tenant per organisation, the provision manager (:11007) provisions what it needs, and four agents do the work (Keycloak client, EDC-V participant context, registration, onboarding). cfm-cp-shim bridges the pinned agents to EDC 0.18's API version. |
ADR-005 |
| Vault | HashiCorp Vault holds every participant's private keys and the EDC secrets. File-backed locally since 2026-09-26 (jad/vault.hcl, with a vault-unseal sidecar), on the Flexible Server on Azure. Never -dev: a restart would take every key. |
ADR-046, ADR-047 |
| Keycloak | OIDC sign-in for the UI and the service clients, realm edcv, :8080. The UI client is confidential with PKCE S256. |
ADR-025 |
| PostgreSQL | The runtime store of the EDC services and Keycloak, one database each (jad/init-postgres.sql). The Flexible Server on Azure. |
ADR-001, ADR-041 |
| NATS | The event mesh (JetStream) between CFM's managers and agents, and between the catalogue crawler and enricher. :4222. |
ADR-020 |
| Traefik | The gateway in front of the JAD services, :80, dashboard on :8090. |
1.4 The hub's own services
| Term | What it is | Decision |
|---|---|---|
| UI | The Next.js app in ui/: every persona's pages and the /api/* routes behind them. Container health-dataspace-ui. |
ADR-004, ADR-044 |
| Neo4j | The knowledge graph of five layers: marketplace (DSP), HealthDCAT-AP, FHIR R4, OMOP CDM and ontologies. :7474 browser, :7687 Bolt. |
ADR-001, ADR-030 |
| neo4j-proxy | services/neo4j-proxy/, Express on :9090: the FHIR and OMOP endpoints the data planes call, the natural-language query engine, the federated query and the catalogue in HealthDCAT-AP. |
ADR-043 |
| Catalog crawler | services/catalog-crawler/, Python: fetches each participant's DSP catalogue every five minutes and publishes it on NATS (dataspace.catalog.raw). |
ADR-020 |
| Catalog enricher | services/catalog-enricher/, Python: reads those catalogues off NATS, maps them to HealthDCAT-AP and merges them into Neo4j as :HealthDataset nodes, so a question about datasets reaches every participant's. |
ADR-020, ADR-003 |
| Klarbefund | The iPhone app in clients/ios/: scans a lab report on the device and connects to the hub as the patient's EHR. |
ADR-033, ADR-049 |
1.5 How they fit together
A participant is created once by CFM: the tenant manager makes the tenant, the agents give it a Keycloak client, a participant context in EDC-V and a DID in the IdentityHub, and the IssuerService issues its credentials. From then on the control plane negotiates on its behalf over DSP, proving who it is over DCP, and a data plane moves the data from the Neo4j proxy once a contract holds. The UI reads the graph directly for its pages and goes through the control plane for anything that is an exchange.
2. Development environment
2.1 Prerequisites
| Need | For |
|---|---|
| Node.js 20 or later, npm | the UI and the Neo4j proxy |
| Docker with Compose v2 | Neo4j, and the full JAD stack; give Docker 8 GB of memory for JAD |
| Python 3.11 or later | the repository's checks (scripts/*.py) and the catalogue services |
pre-commit |
the hooks: pip install pre-commit && pre-commit install --hook-type pre-commit --hook-type pre-push |
| gitleaks, semgrep, actionlint, lychee | optional locally (each hook warns and skips without it); the PR Gate runs what matters |
Xcode 27 and xcodegen |
only for the iPhone app (iOS 26 SDK) |
2.2 Two ways to run it
| Stack | Start | What you get |
|---|---|---|
| Minimal | docker compose up -d neo4j, load the schema and data, cd ui && npm install && npm run dev |
the graph and every page that reads it, at http://localhost:3000; no Keycloak, so sign-in needs the static persona mode |
| Full JAD | docker compose -f docker-compose.yml -f docker-compose.jad.yml up -d, then ./scripts/bootstrap-jad.sh and ./jad/seed-all.sh |
every service in the glossary, the UI container on http://localhost:3003, sign-in through Keycloak |
Loading the graph for the minimal stack (both files are idempotent):
cat neo4j/init-schema.cypher | docker exec -i health-dataspace-neo4j \
cypher-shell -u neo4j -p healthdataspace
cat neo4j/insert-synthetic-schema-data.cypher | docker exec -i health-dataspace-neo4j \
cypher-shell -u neo4j -p healthdataspace
docker compose up -d without a service name also builds the UI as the
container health-dataspace-ui on :3000 and :3003; start only neo4j
when you want npm run dev on :3000.
2.3 The seed phases, in order
./jad/seed-all.sh runs seven phases, and each needs the one before it, so a
failed phase invalidates every later one. Resume with --from N, run one with
--only N.
| Phase | Script | Does |
|---|---|---|
| 1 | seed-health-tenants.sh |
five participant tenants through CFM |
| 2 | seed-ehds-credentials.sh |
the EHDS credential types on the IssuerService |
| 3 | seed-ehds-policies.sh |
ODRL policies for every participant |
| 4 | seed-data-assets.sh |
data assets and contract definitions on EDC-V |
| 5 | seed-contract-negotiation.sh |
PharmaCo to AlphaKlinik negotiations and data planes |
| 6 | seed-federated-catalog.sh |
MedReg to Limburg Medical Centre catalogue negotiation |
| 7 | seed-data-transfer.sh |
EDR tokens and transfers through the data planes |
The runbook is JAD stack seeding.
2.4 Ports
| Port | Service | Port | Service |
|---|---|---|---|
| 3000 | UI (npm run dev or the container) |
11002 | data plane FHIR |
| 3003 | UI container (JAD) | 11003 | control plane, Management API |
| 7474, 7687 | Neo4j browser, Bolt | 11005 | IdentityHub |
| 8080, 9000 | Keycloak, its management port | 11006, 11007 | CFM tenant and provision managers |
| 8200 | Vault | 11012 | data plane OMOP |
| 9090 | neo4j-proxy | 10013 | IssuerService |
| 4222, 8222 | NATS, its monitoring | 10023, 10033 | Simpl GA issuer, rogue issuer |
| 5432 | PostgreSQL | 80, 8090 | Traefik, its dashboard |
2.5 Environment variables
The UI reads its configuration from the environment; ui/.env.example and
ui/.env.local.example are the templates to copy. The ones you meet first:
| Variable | What it sets |
|---|---|
NEO4J_URI, NEO4J_USER, NEO4J_PASSWORD |
the graph; neo4j / healthdataspace is for local development only |
NEO4J_PROXY_URL |
the proxy, http://localhost:9090 by default (ui/src/lib/proxy.ts) |
NEXTAUTH_URL, NEXTAUTH_SECRET |
NextAuth; the secret also signs the sessions the API tests forge |
KEYCLOAK_PUBLIC_URL, KEYCLOAK_INTERNAL_URL, KEYCLOAK_ISSUER, KEYCLOAK_CLIENT_ID, KEYCLOAK_CLIENT_SECRET |
sign-in; never wellKnown (gotcha 5 in CLAUDE.md) |
EDC_MANAGEMENT_URL, EDC_IDENTITY_URL, EDC_ISSUER_URL, EDC_TENANT_URL, EDC_PROVISION_URL |
the EDC-V and CFM services |
NEXT_PUBLIC_STATIC_EXPORT=true |
the GitHub Pages build: no API routes, fixtures from ui/public/mock/ |
2.6 The iPhone app
clients/ios/ is a Swift package and an XcodeGen project; it does not need
the hub to build or to run its tests.
cd clients/ios
brew install xcodegen # once
swift test # the parser and provenance model
xcodegen generate && open MeinBefund.xcodeproj
The simulator build needs an ad-hoc signature (CODE_SIGN_IDENTITY="-") or
every Keychain call fails. A device build and a TestFlight upload need an Apple
Developer team: Scripts/install-device.sh, Scripts/archive-and-upload.sh.
The Klarbefund developer guide has the
rest, and Scripts/run-ui-tests.sh runs the on-screen tests.
2.7 Gotchas worth knowing on day one
The full log, dated, is docs/gotchas.md. The ones that cost
the most time:
- Vault must keep its state. If the IdentityHub logs
Private key ... not found, check thathealth-dataspace-vault-unsealreportedready. - Seed phases are strictly ordered; resume a failed run with
--from. - The static export has no API routes. Every page that reads
/api/...needs a fixture inui/public/mock/, and a test fails without one. - Pre-commit rewrites files (Prettier, the docs facts). Stage them again
and commit again; check that
HEADmoved. - A 200 from Keycloak discovery proves nothing: it is cached. Prove sign-in
with
scripts/azure/check-keycloak-health.sh. - Container Apps cache
:latest; a deploy pushes a new revision rather than restarting.