Back to Docs

Developer Guide

Last updated

Technical documentation for developing, testing, and deploying the Health Dataspace v2 platform — an EHDS regulation reference implementation built on Eclipse Dataspace Components.

Onboarding

New to the project? Follow this quick-start path to get productive within your first day.

1. Get Running

  1. Clone the repository
  2. docker compose up -d (Neo4j)
  3. Seed schema & data (cypher-shell)
  4. cd ui && npm install && npm run dev
  5. Open http://localhost:3000

2. Explore the Platform

  • Switch personas via the User Menu
  • Explore the Graph Explorer (center node)
  • Browse the Data Catalog
  • Check Patient Portal (PATIENT role)
  • Review ODRL policies (HDAB role)

3. Key Concepts

  • DSP: Dataspace Protocol — sovereign data exchange
  • DCP: Decentralised Claims — DID + VC identity
  • FHIR R4: Clinical data standard (EHR)
  • OMOP CDM: Analytics layer for research
  • EHDS: EU regulation for health data sharing
Full Onboarding Guide →

JAD Stack Architecture

The full JAD (Java Application Deployment) stack runs 19 Docker services orchestrated via docker-compose.yml + docker-compose.jad.yml. Services are grouped into five layers:

JAD stack — 25 compose services and their dependencies
ServicePortTraefikPurposeDepends On
Traefik:80 / :8090traefik.localhostAPI gateway & reverse proxy—
PostgreSQL 17:5432—Runtime store (9 databases)—
Vault:8200vault.localhostSecrets, file storage; vault-unseal unseals it after a restart—
Keycloak:8080keycloak.localhostOIDC SSO (realm: edcv, 7 users)PostgreSQL
NATS:4222 / :8222—Async event mesh (JetStream)—
Control Plane:11003cp.localhostDSP protocol + management APIPG, Vault, NATS, KC
Data Plane FHIR:11002dp-fhir.localhostFHIR PUSH transfer typePG, Vault, CP
Data Plane OMOP:11012dp-omop.localhostOMOP PULL transfer typePG, Vault, CP
Identity Hub:11005ih.localhostDCP v1.0 — DID + VC storePG, Vault, KC
Issuer Service:10013issuer.localhostVC issuance + DID:webPG, Vault, KC
Tenant Manager:11006tm.localhostCFM tenant lifecyclePG, KC
Provision Manager:11007pm.localhostCFM resource provisioningPG, KC, CP
Neo4j 5:7474 / :7687—Knowledge graph (APOC + n10s)—
Neo4j Proxy:9090proxy.localhostExpress bridge: UI ↔ Neo4jCP
Next.js UI:3000 / :3003—Application frontendNeo4j

Plus 4 background CFM agents (keycloak, edcv, registration, onboarding) and 1 one-shot seed container. Vault-bootstrap runs as a sidecar.

Prerequisites

Required

  • Node.js 20+ — runtime for UI and proxy
  • Docker Desktop — with Docker Compose V2
  • 8 GB Docker RAM — required for full JAD stack
  • Git — with pre-commit hooks enabled

Optional

  • Python 3.11+ — for Synthea FHIR data loading
  • gitleaks — local secret scanning (brew install gitleaks)
  • lychee — broken link checker (brew install lychee)
  • Playwright browsers — for E2E tests (npx playwright install)

Port Requirements

The JAD stack requires these ports to be free: 80, 3000, 3003, 4222, 5432, 7474, 7687, 8080, 8090, 8200, 8222, 9090, 10013, 11002, 11003, 11005, 11006, 11007, 11012

Quick Start — Minimal Stack

Run Neo4j + Next.js UI with synthetic data. No JAD services needed.

1. Start Neo4j & load schema

docker compose up -d

# Initialize schema (idempotent — safe to re-run)
cat neo4j/init-schema.cypher | \
  docker exec -i health-dataspace-neo4j \
  cypher-shell -u neo4j -p healthdataspace

# Load synthetic data (127 patients, 5300+ nodes)
cat neo4j/insert-synthetic-schema-data.cypher | \
  docker exec -i health-dataspace-neo4j \
  cypher-shell -u neo4j -p healthdataspace

2. Start the UI

cd ui
npm install
npm run dev          # → http://localhost:3000

npm test             # Run the Vitest unit suite
npm run lint         # ESLint (max 55 warnings)

Quick Start — Full JAD Stack

The bootstrap script starts all 25 services with health checks, initializes Vault secrets, imports the Keycloak realm, and runs the 7-phase seed pipeline.

1. Bootstrap everything

# Full stack — takes ~3-5 min on first run
./scripts/bootstrap-jad.sh

# Check status & endpoints
./scripts/bootstrap-jad.sh --status

2. Seed the dataspace

# Run all 7 seed phases (sequential, strict order)
./jad/seed-all.sh

# Resume from a specific phase
./jad/seed-all.sh --from 3

# Run only one phase
./jad/seed-all.sh --only 5

3. Access the platform

# Live UI (production build)
open http://localhost:3003

# Keycloak Admin Console
open http://keycloak.localhost  # admin / admin

# Neo4j Browser
open http://localhost:7474      # neo4j / healthdataspace

# Traefik Dashboard
open http://traefik.localhost

Common operations

./scripts/bootstrap-jad.sh --ui-only   # Rebuild UI only (fast)
./scripts/bootstrap-jad.sh --seed      # Re-run seed pipeline
./scripts/bootstrap-jad.sh --pull      # Pull latest images
./scripts/bootstrap-jad.sh --down      # Stop all services
./scripts/bootstrap-jad.sh --reset     # Stop + remove volumes

Data Seeding Pipeline

The 7-phase seed pipeline populates the dataspace with tenants, credentials, policies, assets, and contracts. Phases must run in strict order — each depends on the previous.

PhaseScriptTarget ServiceWhat It Does
1seed-health-tenants.shTenant ManagerCreate 5 participant tenants via CFM
2seed-ehds-credentials.shIssuer ServiceRegister EHDS credential types
3seed-ehds-policies.shControl PlaneCreate ODRL policies for all participants
4seed-data-assets.shControl PlaneRegister data assets + contracts
5seed-contract-negotiation.shControl PlanePharmaCo ↔ AlphaKlinik negotiations + data planes
6seed-federated-catalog.shControl PlaneMedReg ↔ LMC federated catalog negotiation
7seed-data-transfer.shData PlaneVerify EDR tokens and data plane transfers

Important: Vault secrets are lost on Docker restart (in-memory dev mode). Re-run ./scripts/bootstrap-jad.sh --seed after any docker compose down.

Project Structure

├── .github/workflows/         # CI/CD (pr-gate, test, compliance, security-scan, deploy-azure, pages, ...)
├── connector/                 # EDC-V connector (Gradle multi-module)
│   ├── controlplane/          # DSP + Management API
│   ├── dataplane/             # FHIR + OMOP data planes
│   └── identityhub/           # DCP Identity Hub
├── docs/                      # Architecture docs, journeys, ADRs, reports
├── jad/                       # JAD infrastructure configs
│   ├── keycloak-realm.json    # Keycloak realm (edcv, 7 users, 6 roles)
│   ├── edcv-assets/           # Contract definitions & ODRL policies
│   ├── seed-*.sh              # 7-phase seed scripts
│   └── openapi/               # OpenAPI specifications
├── k8s/                       # Kubernetes / OrbStack manifests
├── neo4j/                     # Cypher scripts & data
│   ├── init-schema.cypher     # Constraints, indexes, vector indexes
│   ├── insert-synthetic-schema-data.cypher
│   └── fhir-to-omop-transform.cypher
├── scripts/                   # Automation (bootstrap, synthea, compliance)
├── services/neo4j-proxy/      # Express bridge (Neo4j ↔ UI)
├── ui/                        # Next.js 15 application
│   ├── src/app/               # 50 pages, 75 API routes
│   ├── src/components/        # Shared React components
│   ├── src/lib/               # auth.ts, api.ts, graph-constants.ts
│   ├── __tests__/unit/        # Vitest unit tests
│   ├── __tests__/e2e/         # Playwright specs (journeys/)
│   └── public/mock/           # 38 JSON fixtures for static export
├── docker-compose.yml         # Minimal stack (Neo4j + UI)
└── docker-compose.jad.yml     # Full JAD stack (22 services)

Neo4j Graph Schema

The 5-layer knowledge graph spans 27 node labels with 70+ indexes and 3 vector indexes for GraphRAG. Schema defined in neo4j/init-schema.cypher (idempotent — safe to re-run).

Core entity-relationship diagram (FHIR ↔ OMOP mapping)

5 Semantic Layers

  • L1 Marketplace: Participant, DataProduct, Contract, HDABApproval, OdrlPolicy
  • L2 HealthDCAT-AP: Catalogue, HealthDataset, Distribution, DataService
  • L3 FHIR R4: Patient, Encounter, Condition, Observation, MedicationRequest
  • L4 OMOP CDM: OMOPPerson, ConditionOccurrence, DrugExposure, Measurement
  • L5 Ontology: SnomedConcept, ICD10Code, RxNormConcept, LoincCode

Key Conventions

  • Labels: PascalCase
  • Relationships: UPPER_SNAKE_CASE
  • Properties: camelCase
  • Always MERGE, never CREATE
  • Constraints use IF NOT EXISTS
  • 3 fulltext indexes (clinical, catalog, ontology search)
  • 3 vector indexes (384-dim, cosine — for GraphRAG)

PostgreSQL Schema

PostgreSQL serves as the runtime store for all JAD services — EDC-V state machines, Keycloak identity, and CFM tenant metadata. Neo4j holds the health knowledge graph. This split follows ADR-1.

DatabaseServiceContents
controlplaneEDC-V Control PlaneContract negotiations, transfer processes, asset definitions, policy store
dataplaneDCore FHIRFHIR data plane state, EDR tokens, transfer tracking
dataplane_omopDCore OMOPOMOP data plane state, EDR tokens, transfer tracking
identityhubIdentity HubDID documents, verifiable credential store, key pairs
issuerserviceIssuer ServiceCredential definitions, attestation records, issued VCs
keycloakKeycloakUsers, roles, realm config, sessions, client scopes
cfmTenant Manager, Provision Manager, CFM agentsTenant records, VPAs (Virtual Participant Agents), provisioning tasks
redlinedb—Created by jad/init-postgres.sql; no service in either compose stack uses it today
taskdbNeo4j proxyPersistent task list behind the proxy's /tasks endpoints (phase 13d)

Neo4j vs PostgreSQL Split

  • Neo4j: Health knowledge graph (FHIR, OMOP, ontologies), graph traversal queries, semantic search, GraphRAG vectors
  • PostgreSQL: EDC-V runtime state machines, OIDC sessions, tenant metadata, credential storage — transactional ACID workloads
  • Rationale: Graph queries for clinical relationships are orders of magnitude faster in Neo4j; EDC-V requires PostgreSQL for its state machine persistence

Integration Flows

Two canonical flows cover 90% of real-world EHDS integrations. Pick the one that matches your role, then use the Scalar API Reference to try each endpoint from the browser.

Data Consumer flow

Researcher / pharma / HTA body discovers and requests cross-border health data.

  1. Discover → GET /api/catalog returns HealthDCAT-AP datasets
  2. Inspect → GET /api/assets for access policies (ODRL)
  3. Negotiate → POST /api/negotiations opens a DSP 2025-1 contract
  4. Attest → POST /api/credentials/present proves role via DCP VC
  5. Transfer → GET /api/transfers/:id monitors the data plane
  6. Analyse → GET /api/analytics or POST /api/nlq
Role: DATA_USER · Persona: Dr. Petra Lang (PharmaCo Research)

Data Provider flow

Hospital / clinic / registry publishes datasets for secondary use.

  1. Register → POST /api/participants creates a did:web identity
  2. Publish → POST /api/catalog adds a HealthDCAT-AP dataset
  3. Policy → POST /api/admin/policies creates an ODRL policy (EDC_ADMIN)
  4. HDAB approval → GET /api/compliance tracks approval state
  5. Accept → GET /api/negotiations shows incoming contract offers
  6. Deliver → data plane pushes FHIR/OMOP bundles to the consumer
Role: DATA_HOLDER · Persona: Dr. Klaus Weber (AlphaKlinik Berlin)

API Reference

75 Next.js API routes (99 operations) proxy to Neo4j and the EDC-V services; the table is generated from ui/src/app/api/. Every route needs a session except the sign-in flows, the health probe and the TestFlight form (ADR-044, ADR-048). Routes are disabled in the static export; mock data is served from ui/public/mock/*.json.

AreaRoutes and methodsWhat it is for
/api/activity-report
/ GET
The access body's activity report, Regulation (EU) 2025/327 Art. 59, generated from the graph
/api/admin
/audit/retention GET POST
/audit GET
/components/[name]/diagnosis GET
/components/[name]/restart POST
/components GET
/components/topology GET
/participants GET POST DELETE
/policies GET POST PUT DELETE
/tenants GET
Operator tools: components and their health, tenants, participants, ODRL policies, audit log and retention. EDC_ADMIN; policies and audit also HDAB_AUTHORITY
/api/analytics
/ GET
OMOP cohort analytics
/api/app-accounts
/challenge POST
/ POST
The Klarbefund app creates a sandbox account for itself, without a session but only with an Apple App Attest attestation (ADR-054)
/api/assets
/ GET POST
EDC asset registry
/api/auth
/[...nextauth] GET POST
/eudi/start POST
/eudi/status GET
Sign-in: NextAuth with Keycloak, and the EUDI Wallet start and status calls. No session needed
/api/catalog
/ GET POST DELETE
HealthDCAT-AP dataset catalogue: list, publish, remove
/api/compliance
/applications/clock POST
/applications/complete POST
/applications GET POST
/findings/close POST
/findings/respond POST
/findings GET POST
/information-requests/answer POST
/information-requests GET POST
/permits/revoke POST
/permits POST
/requests/decide POST
/requests GET POST
/results GET POST
/ GET
/tck GET
Access body workflows: permit applications and their clock, statistical requests, permits and revocation, findings, information requests, results, and the DSP TCK results
/api/credentials
/[id] DELETE
/definitions GET
/request POST
/ GET
Verifiable credentials: list, request, revoke, and the issuer's definitions
/api/debug
/phase26 GET
Read-only check of the federated discovery plumbing. EDC_ADMIN
/api/eehrxf
/ GET
EEHRxF profile alignment
/api/federated
/ GET
Federated query across the participants' graphs
/api/graph
/expand GET
/node GET
/ GET
/validate GET
Knowledge graph: nodes, expansion, schema validation
/api/health
/ GET
Liveness and readiness probe. No session needed
/api/information
/ GET
What the access body tells the public about secondary use, Art. 58(1)
/api/keycloak-config
/ GET
Where Keycloak is, for the sign-in banner. No session needed
/api/mock-dsp
/[participant]/catalog/request GET POST
Demo DSP catalogue endpoint the catalog crawler polls. A session or the crawler's token (ADR-020)
/api/negotiations
/[id] GET
/ GET POST
DSP contract negotiations
/api/nlq
/backend GET
/ GET POST
Natural language queries through the proxy, and which NLP backend answers them
/api/odrl
/scope GET
The caller's effective ODRL scope: permissions, prohibitions, datasets, time limits
/api/overview
/ GET
The data behind the persona overview: layers, nodes, links and signals
/api/participants
/[id]/credentials GET
/[id] PATCH
/me GET
/ GET POST
Participant registry, profiles and their credentials
/api/patient
/app-devices/[deviceId] DELETE
/app-devices GET POST
/app-pairing/[pairingId] GET
/app-pairing POST
/app/account DELETE
/app/record GET POST
/ehr-sync POST
/insights GET
/observations GET
/profile GET
/research GET POST DELETE
/ GET
The patient's record, profile, insights, research consent, EHR sync, and the Klarbefund app pairing and devices
/api/permits
/ GET
The access body's register of applications and permits, Art. 57
/api/tasks
/ GET
Negotiations and transfers across all participant contexts, as one task list
/api/transfers
/[id] GET
/ GET POST
Data transfers
/api/trust-center
/ GET
Trust centers with their governance chain and statistics

Testing

Unit Tests (Vitest)

  • UI and Neo4j proxy suites, the UI's mirroring ui/src/ under ui/__tests__/unit/
  • v8 coverage, published with the test report
  • Testing Library for components; a test that needs the network stubs fetch itself, and Neo4j is mocked at @/lib/neo4j (MSW was removed in #404)
  • View Test Report
npm test               # Run once
npm run test:watch     # Watch mode
npm run test:coverage  # With v8 coverage

E2E Tests (Playwright)

  • Journey specs in ui/__tests__/e2e/journeys/ (IDs J001 onward)
  • WCAG 2.2 AA accessibility audit (27-wcag-accessibility)
  • OWASP/BSI security & pentest (28-security-pentest)
  • Playwright Report
npm run test:e2e       # Headless (chromium)
npm run test:e2e:ui    # Interactive UI

# Against JAD stack
PLAYWRIGHT_BASE_URL=http://localhost:3003 \
  npm run test:e2e

DSP 2025-1 TCK

Validates EDC connector implements Dataspace Protocol correctly — catalog queries, contract negotiations, transfer processes.

./scripts/run-dsp-tck.sh

DCP v1.0

Verifies Decentralized Claims Protocol — DID resolution, credential presentation, trust framework.

./scripts/run-dcp-tests.sh

EHDS Domain

Health domain compliance — FHIR R4 bundles, OMOP transformation, HDAB approval chains, patient rights.

./scripts/run-ehds-tests.sh

EHDS User Journey

The full 8-step EHDS secondary-use journey with sequence diagrams, persona mappings, and E2E test coverage:

View Full User Journey

Quality Gates

Four-stage quality pipeline aligned with BSI C5, OWASP Top 10, EHDS regulation, and WCAG 2.2 AA.

15
Pre-commit hooks
2
Pre-push gates
13
CI jobs
3
Compliance suites
View full Quality Gates documentation →

CI/CD Pipeline

CI/CD: the workstation hooks, the pull request checks, and what runs on main

test.yml — 13 jobs on every push

  • UI Tests (Vitest)
  • Neo4j Proxy Tests (Vitest)
  • ePA Ingest Tests (Vitest)
  • iOS Analyte Parity (Swift)
  • Secret Scan (gitleaks)
  • Dependency Audit
  • SBOM Generation
  • Licence Compliance
  • Trivy Security Scan
  • Kubescape K8s Posture
  • Lint
  • Performance Budget (Lighthouse)
  • E2E Tests (Playwright)

pages.yml — Deploy to GitHub Pages

  1. Seed Neo4j and refresh the mock fixtures from the live API
  2. Run full Vitest suite with coverage
  3. Build Next.js for E2E, run Playwright
  4. Run WCAG 2.2 AA accessibility audit
  5. Run OWASP/BSI security tests
  6. Disable API routes (mv src/app/api /tmp/api_disabled)
  7. Build static export (NEXT_PUBLIC_STATIC_EXPORT=true)
  8. Copy test reports to output
  9. Deploy to GitHub Pages

compliance.yml — Weekly + Push to Main

Runs on the full JAD stack: Image platforms, DSP 2025-1 TCK, DCP v1.0 Compliance, EHDS Health Domain, EHDS API Collection (Bruno), Azure Demo Compliance (ACA Job), Compliance Report. Each suite is held to the floor in scripts/compliance-baseline.json. Scheduled Monday 06:00 UTC, and on pushes to main that touch the scripts, seeds, API routes or the API collection.

deploy-azure.yml — Azure Deployment

Deploys the 21 Container Apps and their jobs to Azure via OIDC federation. Includes E2E smoke tests against the live Azure environment.

reset-demo.yml — Demo Reset (manual)

Restarts stateful services, re-bootstraps Vault and Keycloak, reseeds data and runs smoke tests. Manual dispatch only since 2026-09-13: the Keycloak realm now holds the Klarbefund client and real accounts, which a scheduled reset would delete.

aca-schedule.yml — Off-Hours Stop and Start

Stops every Container App except the UI, then the PostgreSQL Flexible Server, daily at 18:00 UTC; starts them in dependency order Monday to Friday at 05:00 UTC, skipping Berlin public holidays (ADR-053). See Live Stack Off Hours.

Live Stack Off Hours

Outside office hours the Azure stack is stopped, database included, and ehds.mabu.red answers every page with an offline notice and every data route with 503. Local development is not affected: docker compose and npm run dev know nothing of the schedule. To see the notice locally, start the UI with LIVE_DEMO_OFFLINE=true.

Working on the live stack at night or at the weekend

# 1. Hold it: no scheduled stop before this instant (UTC)
gh variable set LIVE_DEMO_HOLD_UNTIL --body "2026-10-10T22:00Z"

# 2. Start it (any day, about ten minutes, green only if a login works)
gh workflow run aca-schedule.yml -f action=start

# 3. Optional: one catalog crawl, it runs in office hours only
az containerapp job start --name mvhd-catalog-crawler --resource-group rg-mvhd-dev

# 4. Done: release the hold and stop it again
gh variable delete LIVE_DEMO_HOLD_UNTIL
gh workflow run aca-schedule.yml -f action=stop
  • Start the stack before merging at night; a deploy expects the apps to be running.
  • A forgotten hold ends by itself: the first scheduled stop after it expires stops the stack.
  • Troubleshooting, the state check and the full procedure are in the off-hours runbook.

Latest Reports

Data Flow

Data pipeline: Synthea → Neo4j → Proxy → UI

Release Notes

All releases

Conventions

  • Commit messages: Conventional Commits format (feat:, fix:, docs:, chore:)
  • Branch strategy: Feature branches → PR to main
  • Pre-commit: 32 hooks, listed on the quality gates page: Prettier, ESLint, TypeScript, Semgrep, gitleaks, the API collection check and more
  • Pre-push: Full Vitest suite (--bail), npm audit (HIGH+)
  • Cypher: UPPER_SNAKE_CASE relationships, PascalCase labels, camelCase properties, always MERGE
  • TypeScript: Strict mode, no any, @/* path alias → ui/src/*
  • Fictional orgs only: AlphaKlinik Berlin, PharmaCo Research AG, MedReg DE, Limburg Medical Centre, Institut de Recherche Santé

Related Documentation