Optimal DX

@crvouga/mockingbird-service-odx

Stateful mock of the (retired) Optimal DX partner API: patients, partner links, HL7 and structured lab imports, Functional Health Reports (JSON/PDF), webhook registrations, and signed PatientTest webhooks.

Work in progress. Usable, but incomplete: operations, response shapes and options can still change between releases. Pin an exact version. Check the README's "Deliberately not modelled" notes before relying on it.
Work in progressHealth & labs Runs in the browser15 of 15 operationsIn-process fetchNode servermockingbird-odx
$npm install -D @crvouga/mockingbird-service-odx

Playground

Requests go to the real mock running in this tab. State persists across requests, and the journal shows what it received. A green dot marks operations whose sample succeeds as-is; others need ids from earlier responses.

Loads the real @crvouga/mockingbird-service-odx into this tab on first send
https://odxinstanceresource.azure-api.net

Send a request to see the mock's response.

Operations

15 of 15 operations in the vendored contract are mocked. Hand-authored from optimal.dx.service.ts / optimal.dx.types.ts, the webhook guard and DTO (odx-signature.guard.ts, odx-webhook-data.dto.ts) and the QA harness (packages/qa odx-client.ts).

MethodPathOperationStatus
GET/v1/partner/labsListPartnerLabs
GET/v1/elements/1ListElements
POST/v1/practice/3f0c0c43-7d2b-4b8e-9a50-9c1f0c6c0001/patientCreatePatient
PUT/v1/practice/3f0c0c43-7d2b-4b8e-9a50-9c1f0c6c0001/patient/100001UpdatePatient
DELETE/v1/practice/3f0c0c43-7d2b-4b8e-9a50-9c1f0c6c0001/patient/100001DeletePatientUsed by the QA harness teardown (via the send-odx-api-request dev tool).
POST/v1/practice/3f0c0c43-7d2b-4b8e-9a50-9c1f0c6c0001/patient/100001/partner/{localUserId}LinkPartnerUserOur client posts the literal body `false`.
GET/v1/practice/3f0c0c43-7d2b-4b8e-9a50-9c1f0c6c0001/patientsListPatientsAll patients, or a search when any filter is given (404 when nothing matches).
POST/v1/practice/3f0c0c43-7d2b-4b8e-9a50-9c1f0c6c0001/patient/100001/testresultsCreateTestResults
POST/v1/practice/3f0c0c43-7d2b-4b8e-9a50-9c1f0c6c0001/patient/100001/testCreatePatientTestImport an HL7 v2 ORU^R01 message; OBX-3 codes map to elements by their elementReferences.
PUT/v1/practice/3f0c0c43-7d2b-4b8e-9a50-9c1f0c6c0001/patient/100001/test/700001UpdatePatientTest
GET/v1/practice/3f0c0c43-7d2b-4b8e-9a50-9c1f0c6c0001/patient/100001/testsListPatientTests
POST/v1/reports/FunctionalHealthReportGenerateFunctionalHealthReport`outputType: Json` answers the report as JSON; `Pdf` answers application/pdf bytes.
GET/v1/webhooksListWebhooksOur webhook guard calls this on every inbound webhook to fetch the signing key.
POST/v1/webhookRegisterWebhook
PUT/v1/webhook/1UpdateWebhook

Documentation

The package README, the same file that ships in the npm tarball.Edit on GitHub

Stateful mock of the Optimal DX (ODX) partner API for test suites: partner labs and their biomarker elements, practice patients (create, update, delete, partner link, search), lab imports (HL7 v2 ORU and structured results), the Functional Health Report (JSON or PDF), webhook registrations, and the signed PatientTest webhooks ODX posts back.

The vendor was retired 2026-07-22. Our queue paths are gated only by the geviti-pdf-enabled flag (default false), so a local stack with no PostHog still calls ODX. The cheaper fix is to turn that flag on through the PostHog mock (@crvouga/mockingbird-service-posthog); use this mock when a suite must exercise the ODX path itself (bio-age webhooks, the Healthie PDF upload, migrations).

  • Operation coverage: SUPPORT.md
  • ODX publishes no spec: openapi.yaml is hand-authored from our consumer's wire shapes (optimal.dx.service.ts, the webhook guard and DTO, and QA's odx-client.ts).

Install

Shell
npm install -D @crvouga/mockingbird-service-odx

ESM only. Node >= 22 or Bun >= 1.2. No native dependencies. Serve it with npx mockingbird-odx serve, createServer from ./server (Node), or createRuntime with any Fetch server.

Usage

Point OPTIMAL_URL at the mock (it is overridable; no path prefix is needed), keep any OPTIMAL_API_KEY and OPTIMAL_PRACTICE_ID. Pre-register the backend's webhook, or let manageWebhooks register it through POST /v1/webhook as it does in production:

Shell
npx mockingbird-odx serve --port 8817 \
  --webhook-url http://127.0.0.1:3000/odx/webhook   # must equal SYSTEM_API_DEPLOYMENT_URL/odx/webhook
TypeScript
import { createRuntime } from "@crvouga/mockingbird-service-odx"

const odx = createRuntime({ webhook: { url: "http://127.0.0.1:3000/odx/webhook" } })
const call = (path: string, body?: unknown) =>
  odx.fetch(
    new Request(`http://odx.test${path}`, {
      method: body === undefined ? "GET" : "POST",
      headers: { ApiKey: "any", "content-type": "application/json" },
      ...(body === undefined ? {} : { body: JSON.stringify(body) }),
    }),
  )

const patient = await (
  await call("/v1/practice/p1/patient", { firstName: "Ada", lastName: "Lovelace", gender: "Female", email: "ada@example.com" })
).json()
// …the app posts HL7 to /v1/practice/p1/patient/{patientId}/test; the mock answers the parsed
// PatientTest and posts a signed `Created` webhook to /odx/webhook.

Routes

Route Behaviour
GET /v1/partner/labs [{labId, name, isCurrentLab}]: AHA (1), Quest (2), LabCorp (3).
GET /v1/elements/{labId} The element corpus (ELEMENTS) for the lab: elementId, elementName, elementGenderType, cuUnit, siUnit, cuToSiConversionFactor, elementReferences[{elementCode}] (LOINC codes HL7 OBX-3 is mapped by). Includes the nine phenotypic-age inputs (ids 494, 496, 506, 511, 537/538, 556, 564, 568, 571). Unknown lab β†’ 404 {Message}.
POST /v1/practice/{pid}/patient Creates an OdxPatient with a numeric patientId (100001…). firstName, lastName, email required, else ASP.NET problem details 400. dateOfBirth becomes YYYY-MM-DDT00:00:00; gender is normalized to Male/Female/Unknown.
PUT /v1/practice/{pid}/patient/{id} Replaces the patient; 404 {Message} when unknown in that practice.
DELETE /v1/practice/{pid}/patient/{id} 204; drops its tests (QA teardown).
POST /v1/practice/{pid}/patient/{id}/partner/{localUserId} Body is the literal false; stores our user id (admin-visible). Answers true.
GET /v1/practice/{pid}/patients All patients; with any of email, firstName, lastName, dateOfBirth it is a case-insensitive search that answers 404 when nothing matches (our client passes ignore404).
POST …/patient/{id}/test HL7 import: {labProfileId, labId, testDate, unitType, userId, externalReference, externalMessageControlId?, externalPatientTestId, menstrualPhase, isFasting, hl7}. OBX-3 code^text maps to an element by lab code, EL<id>, or name (hs-CRP picks the patient's sex); OBX-5 values like <0.2 become comparison: "<". Unmapped / non-numeric observations land in importLogs. Emits Created. A message without MSH or OBX is a 400.
PUT …/patient/{id}/test/{testId} Re-import; emits Updated (ODX sends a Created+Updated pair with identical data).
POST …/patient/{id}/testresults Structured import (results[{elementId, value, comparison}]); emits Created.
GET …/patient/{id}/tests The patient's tests, oldest first.
POST /v1/reports/FunctionalHealthReport outputType: Json β†’ {metadata, labs, elements, sections} with the sections our storeHealthData reads (groups, above/below optimal, "Functional Body Systems" conditions, health concerns); Pdf β†’ a valid single-page application/pdf. Unknown test β†’ 404.
GET /v1/webhooks [{partnerWebhookId, signingKey, createDate, entityEvents: {PatientTest}, webhookUrl}] β€” our guard calls this on every inbound webhook to fetch the key.
POST /v1/webhook, PUT /v1/webhook/{id} Register / update (manageWebhooks).

Errors: missing / unknown ApiKey β†’ 401 {statusCode, message} (Azure API Management's shape); ODX errors β†’ {Message}; validation β†’ ASP.NET problem details {title, status, errors}.

Webhooks

Every test import or re-import posts {entityType: "PatientTest", eventType, data: <PatientTest>} to each registered webhook URL subscribed to that event, with optimaldx-signature: <UPPERCASE hex HMAC-SHA256(signingKey, rawBody)>. data is the full PatientTest (results[].comparison is always a string, as our receiver's zod DTO requires). Non-2xx answers are retried (immediately, 5 s, 5 min, 30 min, 2 h). GET /__admin/webhooks, /__admin/webhooks/events, POST /__admin/webhooks/flush and …/replay work as usual.

Admin (beyond the standard contract)

Route Effect
POST /__admin/tests/:id/webhook {eventType?: Created|Updated|Deleted, signature?: valid|short|bad}: emit a webhook for a stored test.
DELETE /__admin/tests/:id Emit Deleted and drop the test.
GET /__admin/patients, GET /__admin/tests The namespace's records (patients include partnerUserId; the HL7 text is never stored).
GET/PUT /__admin/settings {apiKeys?: string[]} (empty accepts any key).

Fault presets (POST /__admin/faults {"preset": "<name>", "count"?: n}): wrong_length_signature, bad_signature, empty_success, no_content, not_found, server_error, slow, webhook_duplicate, webhook_reorder, webhook_drop.

Namespaces

x-mockingbird-namespace, a /ns/<name> prefix on OPTIMAL_URL, or by API key: PUT /__admin/credentials {"credentials": {"<OPTIMAL_API_KEY>": "<namespace>"}}.

Known consumer bugs

  • Wrong-length signature β†’ 500. OdxSignatureGuard.verifyHmac calls crypto.timingSafeEqual on buffers of different lengths, which throws RangeError instead of returning false, so a truncated optimaldx-signature is a 500 (ODX then retries) rather than a
    1. The wrong_length_signature preset sends a 32-character signature to reproduce it.
  • manageWebhooks "updates" a stale registration with its own old URL (a no-op), then registers the new one, so stale registrations accumulate.
  • updateWebhook passes the method as 'Put'; Node's fetch normalizes it (Bun's does not β€” the consumer port normalizes, as the app runs on Node).

Deliberately not modelled

  • Clinical logic: report conditions and health concerns are derived only from which results fall outside their optimal range; bio-age is not computed.
  • The real element catalog (thousands of elements, lab-specific codes): a 13-element corpus covers our consumer's needs. HL7 segments other than MSH/OBX are ignored; PID is never stored.
  • Report themes, recipients beyond the metadata label, and PDF content.
  • No official SDK exists, so there is no SDK drop-in test.

API

Export Kind Description
OdxAPI class The in-process mock: fetch(request), reset(), emit(testId, eventType, signature?), patients(), tests(). Options: sqlite, now, namespace, settings, onWebhook.
createRuntime function The mock with the full service contract. Options: webhook: {url, signingKey?}, settings, retryDelaysMs, fetch, clock, seed, adminKey, onLog.
ODX_PRESETS object Every named fault preset.
ODX_NAMESPACE string The service name, "odx".
SIGNATURE_HEADER string "optimaldx-signature".
signOdx function (signingKey, body) β†’ UPPERCASE hex HMAC-SHA256.
apiKeyCredential function The ApiKey header (how credentials map to namespaces).
ELEMENTS, LABS arrays The element corpus and partner labs.
matchElement, parseObservations functions HL7 OBX parsing and code β†’ element mapping.
DEFAULT_SETTINGS object Per-namespace defaults.
document, operationIds, supportedOperationIds values The vendored OpenAPI contract and its operation ids.
createServer, serveTarget, DEFAULT_PORT (./server) Node Serve over node:http; the serve CLI target; port 8817.

Part of mockingbird.

↑↓ to navigate↡ to open