Skip to content

Nirmitee Da Vinci DTR Server

CI License: Apache-2.0 DTR 2.2.0

An open-source, payer-side implementation of the HL7 Da Vinci Documentation Templates and Rules (DTR) Implementation Guide, written twice with identical behavior: once in Go and once in .NET 10. Both serve DTR 2.2.0 and DTR STU 2.0.1 side by side and are tested against the Inferno Da Vinci DTR Payer Server test suites. It is the sister project of davinci-crd-server.

Why this exists

The CMS Interoperability and Prior Authorization Final Rule (CMS-0057-F) requires impacted payers to offer a Prior Authorization API, with most API requirements starting in January 2027, and CMS recommends the Da Vinci Burden Reduction implementation guides for it: CRD (Coverage Requirements Discovery), DTR and PAS (Prior Authorization Support).

DTR is the second step in that flow. When CRD tells the clinician that documentation or prior authorization is needed, a DTR client (a SMART app, or DTR logic built into the EHR) asks the payer for its documentation templates and rules:

  • Questionnaires that describe what the payer needs to know,
  • CQL Libraries that pre-populate answers from the patient record, so clinicians only fill real gaps,
  • ValueSets that the answers draw from.

The payer returns them together from Questionnaire/$questionnaire-package. Some forms are adaptive: the payer decides the next question from the answers so far, one step at a time, through Questionnaire/$next-question. The completed QuestionnaireResponse then travels with the prior authorization request. This repository is the payer side of DTR, meant as a working reference that payers, EHR vendors and integrators can read, run, test against and build on.

Inferno results

Tested with the Inferno Da Vinci DTR Test Kit v0.18.0 on 12 September 2026. Report summaries are in docs/results/.

Implementation Suite Run Pass Fail Of which known kit issues Skip or omit Report
Go DTR v2.2.0 local, HTTP 42 3 3 (ENV-1, KIT-1, KIT-2) 1 summary
.NET DTR v2.2.0 local, HTTP 42 3 3 (ENV-1, KIT-1, KIT-2) 1 summary
Go DTR v2.0.1 local, HTTP 17 2 2 (KIT-3a, KIT-3b) 0 summary
.NET DTR v2.0.1 local, HTTP 17 2 2 (KIT-3a, KIT-3b) 0 summary
Go DTR v2.2.0 public inferno.healthit.gov, HTTPS 43 2 2 (KIT-1, KIT-2) 1 summary, session
Go DTR v2.0.1 public inferno.healthit.gov, HTTPS 17 2 2 (KIT-3a, KIT-3b) 0 summary, session
.NET DTR v2.2.0 public inferno.healthit.gov, HTTPS 43 2 2 (KIT-1, KIT-2) 1 summary, session
.NET DTR v2.0.1 public inferno.healthit.gov, HTTPS 17 2 2 (KIT-3a, KIT-3b) 0 summary, session

Every failure in every run is a known kit issue: there are no unexpected failures or errors. The single omit is v2.2.0 3.4.06 (no contained Binary resources in the sample QuestionnaireResponses), which is not a failure.

Screenshots from the public Inferno sessions

Captured on inferno.healthit.gov (DTR test kit v0.18.0) from the sessions linked above. The suite-level red mark comes only from the known kit issues listed in the next section; every other group passes.

Suite overview (Go left, .NET right)

Go .NET
Go DTR server on the Inferno DTR Payer Server v2.2.0 suite: Discovery, Backend Services and Log Questionnaire Error groups pass .NET DTR server on the Inferno DTR Payer Server v2.2.0 suite: Discovery, Backend Services and Log Questionnaire Error groups pass
Go DTR server on the Inferno DTR Payer Server v2.0.1 suite: Static Questionnaire Package Retrieval passes .NET DTR server on the Inferno DTR Payer Server v2.0.1 suite: Static Questionnaire Package Retrieval passes

v2.2.0 Backend Services, including the token endpoint TLS test, passed over HTTPS

Go DTR server passing the SMART on FHIR discovery and backend services authorization groups on public Inferno

v2.2.0 Questionnaire Operations: 3.1 to 3.6 pass; 3.7 Must Support fails only on KIT-1 and KIT-2 (#130)

Go DTR server passing questionnaire package, next-question, design, ValueSet expand and error handling groups, with Must Support failing on the known kit issue

v2.0.1 Adaptive Questionnaire Retrieval: 2.01 to 2.06 pass; 2.07 and 2.08 fail with "No resources to validate." because an earlier test clears their data (#131)

Go DTR server passing adaptive questionnaire tests 2.01 to 2.06, with 2.07 and 2.08 failing on the known kit issue

The failures in the "known kit issues" column are listed in the next section. They are counted as failures, not hidden. Local runs use plain HTTP, so the v2.2.0 test that checks the token endpoint for TLS 1.2 or higher cannot pass there; on the public Inferno instance the servers sit behind an HTTPS tunnel and that test runs over TLS. Passing Inferno is a conformance signal, not a certification.

Known Inferno test kit issues (open upstream)

This is the open state as of 12 September 2026 with kit v0.18.0. The servers follow the DTR profiles rather than work around the kit, so these tests fail for any conformant server until the kit is fixed.

Suite Test What happens Upstream issue
v2.2.0 dtr_v220_payer_questionnaire_base_must_support The generated must support list requires Questionnaire.extension:assemble-expectation, but the DTR 2.2.0 profiles (dtr-base-questionnaire, dtr-std-questionnaire, dtr-questionnaire-adapt) set that slice to max 0. Adding the extension makes this test pass and the package and $next-question profile validation tests fail. davinci-dtr-test-kit#130
v2.2.0 dtr_v220_payer_questionnaire_adaptive_search_must_support The list requires item, the item.* elements and assemble-expectation, but dtr-questionnaire-adapt-search 2.2.0 sets Questionnaire.item and assemble-expectation to max 0. davinci-dtr-test-kit#130
v2.0.1 payer_server_adaptive_response_bundles_validation_test An earlier test in the adaptive group (the adaptive expressions test) clears the stored adaptive questionnaire bundles, so this test fails with "No resources to validate." davinci-dtr-test-kit#131
v2.0.1 payer_server_adaptive_response_search_validation_test Same cleared data; fails with "No questionnaire bundles to validate." davinci-dtr-test-kit#131

A v2.0.1 run with the input retrieval_method=Static skips every adaptive test, including these two, but also skips the adaptive coverage, so the published runs keep Both.

One IG snapshot quirk is avoided rather than listed: the DTR 2.0.1 dtr-qpackage-output-parameters snapshot copies the Bundle invariants bdl-3 and bdl-4 onto the Parameters wrapper, so any package Bundle wrapped in Parameters fails validation. For a single package without warnings the v2.0.1 base returns the bare Bundle, which the 2.0.1 operation allows. The evidence for every entry is in spec/SPEC.md section 12 and harness/known_kit_issues.json.

Features

  • Two DTR versions in one process: DTR 2.2.0 under /v220/fhir and STU 2.0.1 under /v201/fhir, each with its own CapabilityStatement and payer content.
  • SMART Backend Services authorization: .well-known/smart-configuration and a token endpoint for client_credentials with private_key_jwt. Client assertions must be signed with RS384 or ES384 by a key from a trusted JWK Set (refetched once on an unknown kid), with iss and sub equal to a registered client id, aud equal to the token endpoint, exp at most five minutes ahead and an unused jti. Access tokens are opaque and short-lived.
  • Questionnaire/$questionnaire-package: resolves the Coverage to an active member (422 otherwise), selects Questionnaires by canonical (with optional version), by context id or by order code (DeviceRequest, ServiceRequest and the other DTR order types), removes duplicates and reports unknown canonicals as a warning outcome. Each package Bundle holds the Questionnaire first, then (2.2.0) an initial in-progress QuestionnaireResponse with qr-coverage, intendedUse and qr-context, then its Libraries and ValueSets.
  • CQL packaging: Libraries carry base64 CQL and ELM, Questionnaire expressions use CQL with an ELM alternativeExpression (2.2.0), and every cqf-library and answerValueSet reference is versioned and resolves inside the same bundle.
  • Adaptive Questionnaire/$next-question: stateless. The server validates the posted QuestionnaireResponse (contained form, link ids, answer types, repeats, answer options, enabled items; item issues are returned together with an expression), applies the flow rules, and returns the response with the contained form grown by the next questions. The status becomes completed when nothing new is shown and every required item is answered.
  • ValueSet/$expand by GET or POST, returning the expanded ValueSet with a UTC timestamp, and Questionnaire/$log-questionnaire-errors, recorded in the audit log.
  • FHIR errors as OperationOutcomes: 400 for invalid input, 401 with WWW-Authenticate: Bearer, 404 for unknown routes and ValueSets, 422 for source data errors such as an unknown or inactive coverage.
  • Sample payer content as data: a hospital bed Questionnaire (HCPCS E0250) with CQL pre-population and enableWhen logic, and an adaptive sleep study Questionnaire (CPT 95810), with their Libraries, ValueSets, payer directory and adaptive flow in spec/content/. All members and identifiers are synthetic.
  • Reference engine and golden examples: harness/dtrgen/engine.py is an executable reading of the spec. It generates the content, the Inferno inputs and the golden exchanges in spec/examples/, which the Go and .NET unit tests replay through their HTTP handlers.
  • External base URL per request from DTR_PUBLIC_BASE_URL or X-Forwarded-Proto and X-Forwarded-Host, so advertised endpoints, fullUrls and the adaptive endpoint stay correct behind proxies and tunnels.
  • Audit log: one JSON line per request in a daily file. Access tokens and client assertions are never written.
  • Library plus host: no global state and a configurable route prefix, so the DTR module can be mounted next to CRD and PAS in one process. Docker images run as a non-root user.

Architecture

Each implementation is a DTR library, a shared package for cross-cutting concerns and a thin host.

flowchart LR
  CLIENT["DTR client: SMART app or EHR<br/>(Inferno during testing)"]
  CONTENT[("spec/content<br/>payer directory, Questionnaires,<br/>Libraries, ValueSets, adaptive flows")]
  JWKS[("Client JWK Sets")]

  subgraph proc["DTR server process"]
    direction TB
    HOST["Host<br/>Go: cmd/dtr-server<br/>.NET: Nirmitee.DaVinci.Dtr.Host"]
    subgraph lib["DTR library (Go: dtr, .NET: Nirmitee.DaVinci.Dtr)"]
      DISC["CapabilityStatement and<br/>SMART configuration"]
      TOKEN["Token endpoint and<br/>bearer checks"]
      PKG["$questionnaire-package"]
      NEXT["$next-question"]
      TERM["ValueSet/$expand,<br/>$log-questionnaire-errors"]
      STORE["Content store with<br/>per-request FHIR base"]
    end
    subgraph shared["Shared (Go: internal/shared, .NET: Nirmitee.DaVinci.Common)"]
      AUTH["Client assertion verifier,<br/>JWKS cache, jti replay cache"]
      FJSON["FHIR JSON helpers"]
      AUD["JSONL audit log"]
    end
  end

  CLIENT -- "token request with client assertion" --> HOST
  CLIENT -- "FHIR operations with bearer token" --> HOST
  HOST --> lib
  TOKEN --> AUTH
  AUTH -. "fetch keys" .-> JWKS
  PKG --> STORE
  NEXT --> STORE
  TERM --> STORE
  STORE --> CONTENT
  PKG --> FJSON
  lib --> AUD
Loading

The Go internal/shared package and the .NET Nirmitee.DaVinci.Common project are copies of the ones in the CRD server with the same public API, plus the SMART client assertion verifier and token store that DTR needs. That keeps the servers independent today and makes the shared module a mechanical extraction later.

How DTR fits with the CRD and PAS servers:

flowchart TB
  subgraph flow["Prior authorization flow (one repository and deployable server per IG)"]
    CRD["davinci-crd-server<br/>CDS Hooks: coverage, PA and documentation needs"]
    DTR["davinci-dtr-server (this repository)<br/>Questionnaires, CQL, adaptive forms"]
    PAS["davinci-pas-server (planned)<br/>prior authorization submit and inquire"]
  end
  COMMON["Shared module (planned)<br/>JWT and JWKS auth, FHIR helpers, audit log, config"]
  COMBINED["Combined Burden Reduction server (planned)<br/>/crd, /dtr and /pas in one process"]

  CRD -- "documentation needed: which Questionnaires" --> DTR
  DTR -- "completed QuestionnaireResponse" --> PAS
  CRD --> COMMON
  DTR --> COMMON
  PAS --> COMMON
  CRD --> COMBINED
  DTR --> COMBINED
  PAS --> COMBINED
Loading

Mounting the DTR module in your own host:

cfg, err := dtr.LoadConfig("", os.Getenv)
if err != nil { log.Fatal(err) }
cfg.Prefix = "/dtr"
handler, err := dtr.NewHandler(cfg)
if err != nil { log.Fatal(err) }
mux.Handle("/dtr/", handler) // /dtr/v220/fhir, /dtr/v201/fhir, /dtr/health
builder.Services.AddDtr(builder.Configuration);
app.MapDtr("/dtr");   // /dtr/v220/fhir/..., /dtr/v201/fhir/..., /dtr/health

The full behavior contract both implementations follow is spec/SPEC.md. Implementation details are in go/README.md and dotnet/README.md.

Quick start

git clone https://github.com/Nirmitee-tech/davinci-dtr-server.git
cd davinci-dtr-server

Go (native)

Requires Go 1.26.

cd go
go run ./cmd/dtr-server
curl http://localhost:8183/v220/fhir/metadata

.NET (native)

Requires the .NET 10 SDK.

cd dotnet
dotnet run --project src/Nirmitee.DaVinci.Dtr.Host -- --urls http://0.0.0.0:8084
curl http://localhost:8084/v220/fhir/metadata

Docker

Build from the repository root so spec/content is in the build context.

docker build -f go/Dockerfile -t davinci-dtr-server-go .
docker run --rm -p 8183:8183 davinci-dtr-server-go

docker build -f dotnet/Dockerfile -t davinci-dtr-server-dotnet .
docker run --rm -p 8084:8084 davinci-dtr-server-dotnet

Inside a container http://localhost is the container itself. When the trusted JWK Sets are served on the host (as with a local Inferno on port 8180), rewrite them: -e DTR_URL_REWRITES="http://localhost:8180|http://host.docker.internal:8180" for Go, or -e Dtr__JwksUrlRewrites__0__From=http://localhost:8180 -e Dtr__JwksUrlRewrites__0__To=http://host.docker.internal:8180 for .NET.

A first package request

Discovery needs no token:

curl -s http://localhost:8183/v220/fhir/.well-known/smart-configuration

The v2.2.0 operations need an access token from the token endpoint, which requires a signed client assertion. For a quick local try, start the server with a test-only static token on the v2.0.1 base (static tokens are off by default; never enable one outside local testing, see SECURITY.md) and post the synthetic static package request:

# start the server with, for example: DTR_V201_STATIC_TOKENS=local-test-token go run ./cmd/dtr-server
curl -s -X POST 'http://localhost:8183/v201/fhir/Questionnaire/$questionnaire-package' \
  -H "Authorization: Bearer $LOCAL_TEST_TOKEN" \
  -H 'Content-Type: application/fhir+json' \
  --data @testdata/v201/initial_static_questionnaire_request.json

The response is the hospital bed package: the Questionnaire, its CQL Library and its ValueSet in one Bundle.

Configuration

Environment variables shared by both implementations:

Variable Default Meaning
DTR_LISTEN :8183 (Go), :8084 (.NET) listen address; -listen (Go) or --urls (.NET) overrides it
DTR_PUBLIC_BASE_URL unset fixed external origin, for example https://<tunnel-host>
DTR_CONTENT_DIR spec/content payer content directory
DTR_CLIENT_IDS nirmitee-dtr-inferno accepted client ids (assertion iss and sub), comma separated
DTR_TRUSTED_JWKS_URLS local and public Inferno JWK Sets JWK Sets used to verify client assertions, comma separated
DTR_JWKS_FILE unset local JWK Set checked before the URLs
DTR_TOKEN_TTL_SECONDS 300 access token lifetime
DTR_V201_STATIC_TOKENS empty (opt-in) extra bearer tokens accepted on the v2.0.1 base; the Inferno v2.0.1 test data needs nirmitee-dtr-v201-static-token for local test runs only, never set it on a real deployment
DTR_AUDIT_DIR data/dtr-audit audit log directory; lines go to {dir}/YYYY-MM-DD.jsonl

The default trusted JWK Sets are the Inferno SMART App Launch kit keys (/custom/smart_stu2/.well-known/jwks.json) and the inferno_core keys (/jwks.json) on http://localhost:8180 and https://inferno.healthit.gov/suites. Replace them, the client id and the static token with your real clients in production.

The v2.0.1 base also accepts tokens issued by either base and the static tokens, because the v2.0.1 suite sends a bearer token from its inputs and never requests one itself.

Implementation-specific settings:

Setting Go .NET
Config file -config path or DTR_CONFIG_FILE (YAML or JSON) appsettings.json, Dtr section
Route prefix DTR_PREFIX Dtr__PathPrefix
Outbound JWKS URL rewrites DTR_URL_REWRITES (from|to pairs) Dtr__JwksUrlRewrites__N__From, __To
JWT clock skew DTR_CLOCK_SKEW_SECONDS Dtr__ClockSkewSeconds
Log level DTR_LOG_LEVEL Logging__LogLevel__Default
Operations without a token (development only) DTR_AUTH_DISABLED=true not available

The Docker images set DTR_AUDIT_DIR=/data/dtr-audit and point DTR_CONTENT_DIR at the content copied into the image.

Running the Inferno suites yourself

The test kit is not part of this repository. You need Docker with Compose 2.24.4 or newer, and Python 3.

# 1. clone the Inferno DTR test kit in the repository root (the folder is gitignored)
git clone https://github.com/inferno-framework/davinci-dtr-test-kit.git

# 2. add davinci-dtr-test-kit/docker-compose.override.yml (below), then start the kit on port 8180
(cd davinci-dtr-test-kit && COMPOSE_PROJECT_NAME=inferno-dtr sh setup.sh && docker compose -p inferno-dtr up -d)

# 3. run a server natively on the host (Go on 8183 or .NET on 8084), then run the suites
python3 harness/run_suite.py --version v220 --impl go
python3 harness/run_suite.py --version v201 --impl go

The override publishes Inferno on http://localhost:8180, where the harness and the servers' default trusted JWK Sets expect it, and gives the HL7 validator enough memory for both DTR IG versions:

name: inferno-dtr
services:
  inferno:
    environment:
      INFERNO_HOST: http://localhost:8180
  worker:
    environment:
      INFERNO_HOST: http://localhost:8180
  nginx:
    ports: !override
      - "8180:80"
  hl7_validator_service:
    mem_limit: 6g
    environment:
      JAVA_TOOL_OPTIONS: -Xmx5g

--impl dotnet targets port 8084. Useful options: --groups 1,2 to run selected groups, --input NAME=VALUE to override an input (for example retrieval_method=Static on v2.0.1), --server-base for the origin Inferno should call, and --inferno https://inferno.healthit.gov/suites to drive the public instance. Reports are written to harness/reports/. The exit status is 0 for no failures, 1 for unexpected failures, 2 for a harness problem and 3 when the only failures are the known kit issues. python3 harness/build_dtr_content.py rebuilds the content, the test inputs and the golden examples.

To run the TLS test, expose the server over HTTPS, for example with ngrok http 8183, start the server with DTR_PUBLIC_BASE_URL=https://<ngrok-host>, and run the suites on the public Inferno instance with --server-base https://<ngrok-host>. The step-by-step guide is harness/tls_public_run.md.

Repository layout

go/             Go implementation: dtr/ library (content store, engine), cmd/dtr-server host, internal/shared
dotnet/         .NET 10 implementation: Nirmitee.DaVinci.Dtr library, Dtr.Host, Common, xUnit tests
spec/           SPEC.md behavior contract, content/ payer content and CQL sources, examples/ golden exchanges
testdata/       synthetic Inferno inputs for the v2.2.0 and v2.0.1 suites
harness/        suite runner, content generator and reference engine, known kit issues, TLS run guide
docs/results/   curated Inferno report summaries

Roadmap

  • PAS server: Da Vinci Prior Authorization Support server (submit, inquire, subscriptions and notifications), Go and .NET, tested against the Inferno PAS server suites.
  • Shared module: replace the copies of the shared code in the CRD and DTR servers with one module for JWT and JWKS auth, SMART Backend Services, FHIR helpers, the audit log and configuration.
  • Combined server: one Burden Reduction host that mounts CRD, DTR and PAS under their own prefixes.
  • Known kit issues: remove each entry once the upstream test kit fix is released and the suites are rerun.

Contributing

Contributions are welcome. Behavior changes start in spec/SPEC.md and land in both implementations. See CONTRIBUTING.md for setup, tests and the pull request checklist, and follow the Code of Conduct. Report security issues privately as described in SECURITY.md.

All data in this repository is synthetic. Please never post real patient data in issues or pull requests.

License

Apache License 2.0. See LICENSE and NOTICE.

HL7, FHIR and the FHIR [FLAME DESIGN] are the registered trademarks of Health Level Seven International and their use does not constitute endorsement by HL7. The Inferno test kit is developed by the Inferno Framework team and is not redistributed here.

Built by Nirmitee.io.

About

Open-source Da Vinci DTR payer server in Go and .NET. Passes the Inferno DTR Payer Server suites v2.2.0 and v2.0.1; remaining failures are open upstream kit issues.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages