ARCHITECTURE
Last updated: 10 April 2026
Overview
The HOW Integration Platform orchestrates blood-test workflows for House of Wellness UK. It connects three external systems — Randox (laboratory), OptimalDX (report interpretation), and GoHighLevel/CRM (patient identity & communications) — through a central Azure-hosted backend, with a private staff portal for operator access.
Production URL: https://results.houseofwellnessuk.com
1. High-Level Architecture
graph TB
subgraph "Staff Portal"
SWA["Azure Static Web App<br/>(React + Vite)"]
end
subgraph "Azure Backend"
FUNC["Azure Functions<br/>(.NET 10, isolated worker)"]
TS["Table Storage<br/>(Cases, Tags, Notes, Bookings)"]
BS["Blob Storage<br/>(artifacts container)"]
KV["Azure Key Vault<br/>(API keys, credentials)"]
end
subgraph "External Systems"
RANDOX["Randox Health<br/>(Laboratory API)"]
ODX["OptimalDX<br/>(Report Engine API)"]
GHL["GoHighLevel / CRM<br/>(Patient Records + Comms)"]
end
SWA -->|"/api/* (SWA proxy)"| FUNC
FUNC -->|"Read/Write entities"| TS
FUNC -->|"Read/Write artifacts"| BS
FUNC -->|"DefaultAzureCredential"| KV
FUNC -->|"Orders, results, catalogue"| RANDOX
FUNC -->|"Patients, HL7 results, reports"| ODX
FUNC -->|"Contacts, emails, SMS"| GHL
style SWA fill:#e8f5e9
style FUNC fill:#e3f2fd
style RANDOX fill:#fff3e0
style ODX fill:#fce4ec
style GHL fill:#f3e5f5
2. Azure Resource Layout
graph LR
subgraph "prod-spoke-uks (Production)"
subgraph "how-prod-integration-rg"
PF["how-prod-api-func<br/>Function App (Y1, UK South)"]
PS["how-prod-portal<br/>Static Web App (West Europe)"]
PST["howprodintegration<br/>Storage Account"]
PKV["how-prod-kv<br/>Key Vault"]
end
end
DNS["results.houseofwellnessuk.com"] --> PS
PS -->|"linked backend"| PF
PF -->|"Managed Identity"| PKV
PF -->|"Managed Identity"| PST
3. End-to-End Case Lifecycle
sequenceDiagram
participant Staff as Portal Staff
participant API as Functions API
participant CRM as GoHighLevel
participant R as Randox
participant ODX as OptimalDX
participant Store as Azure Storage
Note over Staff,Store: 1. Case Creation
Staff->>API: Search/create patient
API->>CRM: GET/POST contacts
CRM-->>API: Contact details
Staff->>API: POST /api/cases
API->>Store: Insert CaseEntity (Table)
Note over Staff,Store: 2. Randox Order
Staff->>API: Create Randox order
API->>CRM: Get patient demographics (DOB, sex)
API->>R: POST CreatePendingOrder
R-->>API: { orderId, orderNumber }
API->>R: POST UpdateOrder (sample details)
API->>Store: Update case (randoxOrderId, status=Submitted)
API->>Store: Generate TOF PDF (Blob)
Note over Staff,Store: 3. Automated Polling (hourly)
loop Every hour (timer trigger)
API->>Store: Load in-flight cases (status 2/3)
API->>R: POST GetOrderStatus
R-->>API: { statusId, statusDescription }
API->>Store: Update case status
end
Note over Staff,Store: 4. Results & Report (automated on completion)
R-->>API: Status = Completed (≥4)
API->>R: POST GetOrderResultDetail
R-->>API: Results JSON
API->>Store: Save randox/raw/{caseId}.json (Blob)
alt Standard blood panel
API->>ODX: POST patient (create/sync)
API->>ODX: POST patient test (HL7 ORU^R01)
API->>Store: Save odx/hl7/{caseId}.hl7 (Blob)
API->>ODX: POST FunctionalHealthReport
ODX-->>API: PDF bytes
API->>Store: Save odx/pdf/{caseId}/report.pdf (Blob)
else PDF-only panel (microbiome/NGS)
API->>R: POST GetOrderResultReports
R-->>API: Base64 PDF
API->>Store: Save randox/pdf/{caseId}/report.pdf (Blob)
end
API->>CRM: Upload PDF attachment
API->>CRM: Send Email + SMS to patient
API->>Store: Update case (emailed timestamps)
4. Data Flow — Randox Integration (Deep Dive)
flowchart LR
subgraph "HOW Backend"
AUTH["Auth: POST /auth/token<br/>(username + password + client_id)"]
CREATE["CreatePendingOrder<br/>→ patient demographics<br/>→ panels, tests, reasons<br/>→ externalNumber (FML001-XXXXXXXX)"]
UPDATE["UpdateOrder<br/>→ sample collection details<br/>→ tubes, lab, date"]
POLL["GetOrderStatus<br/>→ orderId / orderNumber<br/>← statusId (2=Submitted,<br/>3=Pending, 4+=Complete)"]
PULL["GetOrderResultDetail<br/>→ orderId / orderNumber<br/>← analytes, results, units,<br/>ref ranges, dates"]
PDF["GetOrderResultReports<br/>→ orderId / orderNumber<br/>← base64 PDF"]
end
subgraph "Randox API"
RAPI["api-e endpoints<br/>(APIM gateway)"]
end
subgraph "Storage"
BLOB_RAW["randox/raw/{caseId}.json"]
BLOB_PDF["randox/pdf/{caseId}/*.pdf"]
BLOB_TOF["randox/tof/{caseId}.pdf"]
TABLE["Cases table<br/>(randoxOrderId, randoxOrderRef,<br/>randoxOrderStatusId, etc.)"]
end
AUTH --> RAPI
CREATE --> RAPI
UPDATE --> RAPI
POLL --> RAPI
PULL --> RAPI
PDF --> RAPI
PULL --> BLOB_RAW
PDF --> BLOB_PDF
CREATE --> TABLE
UPDATE --> BLOB_TOF
POLL --> TABLE
style RAPI fill:#fff3e0
Randox Authentication
| Field |
Source |
username |
Key Vault: Randox-Prod-Username |
password |
Key Vault: Randox-Prod-Password |
client_id |
Key Vault: Randox-Prod-ClientID |
subscription_key |
Key Vault: Randox-SubscriptionKey (header: Ocp-Apim-Subscription-Key) |
| Base URL |
Key Vault: Randox-Prod-BaseURL |
Randox Order Status Codes
| Code |
Description |
Action |
| 2 |
Submitted |
Poll continues |
| 3 |
Pending Results |
Poll continues |
| 4+ |
Completed |
Pull results, trigger downstream |
5. Data Flow — OptimalDX Integration (Deep Dive)
flowchart LR
subgraph "HOW Backend"
SYNC["Sync Patient<br/>→ firstName, lastName,<br/>DOB, gender, email<br/>→ externalPatientId = crmContactId"]
HL7["Submit Results (HL7 ORU^R01)<br/>→ labProfileId, testDate<br/>→ OBX segments per analyte<br/>→ code^name^99RDX system"]
FHR["Generate Report<br/>→ practiceId, patientId,<br/>patientTestId, reportIds[]<br/>← PDF bytes"]
UPLOAD["Upload PDF<br/>→ multipart file upload<br/>→ patientId, type=BloodTestResults"]
end
subgraph "OptimalDX API"
OAPI["REST API v1<br/>(apiKey header auth)"]
end
subgraph "Storage"
BLOB_HL7["odx/hl7/{caseId}.hl7"]
BLOB_PDF["odx/pdf/{caseId}/*.pdf"]
TABLE["Cases table<br/>(odxPatientId, odxPatientTestId,<br/>odxReportId, etc.)"]
end
SYNC --> OAPI
HL7 --> OAPI
FHR --> OAPI
UPLOAD --> OAPI
HL7 --> BLOB_HL7
FHR --> BLOB_PDF
SYNC --> TABLE
HL7 --> TABLE
style OAPI fill:#fce4ec
OptimalDX Authentication
| Field |
Source |
apiKey |
Key Vault: OptimalDX-Prod-ApiKey (sent as header) |
practiceId |
Key Vault: OptimalDX-Prod-PracticeID |
| Base URL |
Key Vault: OptimalDX-Prod-baseURL → normalized to {baseUrl}/v1 |
HL7 Message Structure (ORU^R01 v2.3)
| Segment |
Content |
| MSH |
Sender: HOW, Receiver: OptimalDX |
| PID |
Patient ID (MR identifier) |
| OBR |
Order number, "RANDOX^Blood Test Results^L" |
| OBX (per analyte) |
Value type (NM/ST), code^name^99RDX, result, units, reference range, abnormal flag (H/L) |
HbA1c Rule
- Always use OptimalDX Element ID 781 (mmol/mol, IFCC standard)
- Element ID 540 (%) is NOT used
- No unit conversion performed
6. Data Flow — CRM / GoHighLevel (Deep Dive)
flowchart LR
subgraph "HOW Backend"
SEARCH["Search Contacts<br/>→ query, limit"]
GET["Get Contact<br/>→ contactId"]
CREATE_C["Create Contact<br/>→ name, email, phone,<br/>DOB, customFields"]
UPDATE_C["Update Contact<br/>→ partial fields"]
UPLOAD_F["Upload Attachment<br/>→ multipart PDF file"]
EMAIL["Send Email<br/>→ type=Email, from, subject,<br/>html, attachments[]"]
SMS["Send SMS<br/>→ type=SMS, message"]
end
subgraph "GoHighLevel API"
GAPI["LeadConnector<br/>REST API v2021-07-28"]
end
subgraph "Portal"
UI["Staff Portal<br/>(patient search, case management)"]
end
UI --> SEARCH
UI --> CREATE_C
SEARCH --> GAPI
GET --> GAPI
CREATE_C --> GAPI
UPDATE_C --> GAPI
UPLOAD_F --> GAPI
EMAIL --> GAPI
SMS --> GAPI
style GAPI fill:#f3e5f5
CRM Authentication
| Mode |
Config |
Secrets |
| OAuth2 (recommended) |
CrmAuthMode=oauth, CrmOAuthTokenUrl |
GHL-OAuth-ClientId, GHL-OAuth-ClientSecret, GHL-OAuth-RefreshToken |
| PIT (legacy) |
CrmAuthMode=pit |
GHL-PrivateToken |
Custom Field Mappings
| Field |
App Setting |
Purpose |
| Date of Birth |
CrmDobCustomFieldId |
Required for Randox orders |
| Biological Sex |
CrmBiologicalSexCustomFieldId |
Required for Randox orders |
| Athlete |
CrmAthleteCustomFieldId |
Optional: ODX report context |
7. Data Storage Map
Table Storage
| Table |
PartitionKey |
RowKey |
Purpose |
Cases |
CrmLocationId |
CaseId (GUID) |
Core case workflow state, Randox/ODX mappings, artifact paths |
ContactNotes |
CrmContactId |
NoteId (GUID) |
Free-text clinical notes per patient |
Tags |
"tag" |
TagId (GUID) |
Case grouping/categorisation labels |
SandboxClinicalBookings |
varies |
varies |
Sandbox booking audit log |
Blob Storage (artifacts container)
| Path Pattern |
Content |
Written By |
randox/raw/{caseId}.json |
Raw Randox results JSON |
Poller / manual pull |
randox/pdf/{caseId}/{filename}.pdf |
Randox report PDF (microbiome/NGS/health check) |
RandoxReportPdfService |
randox/tof/{caseId}.pdf |
Test Order Form PDF (barcode label) |
RandoxTestOrderFormService |
odx/hl7/{caseId}.hl7 |
HL7 ORU^R01 message submitted to ODX |
OdxResultsSubmissionService |
odx/pdf/{caseId}/{filename}.pdf |
OptimalDX Functional Health Report PDF |
OdxReports |
Key Vault Secrets
| Category |
Secret Names |
| Randox |
Randox-Prod-BaseURL, Randox-Prod-Username, Randox-Prod-Password, Randox-Prod-ClientID, Randox-SubscriptionKey |
| OptimalDX |
OptimalDX-Prod-ApiKey, OptimalDX-Prod-PracticeID, OptimalDX-Prod-baseURL |
| CRM/GHL |
GHL-OAuth-ClientId, GHL-OAuth-ClientSecret, GHL-OAuth-RefreshToken, GHL-PrivateToken |
| Clinical Booking |
Randox-ClinicalBooking-SubscriptionKey, Randox-ClinicalBooking-ClientID |
8. Automation & Polling
flowchart TD
TIMER["Timer Trigger<br/>Every hour (0 0 */1 * * *)"] --> LOAD["Load all cases<br/>from Table Storage"]
LOAD --> FILTER["Filter: has Randox order<br/>AND status 2/3 (in-flight)<br/>OR status 4+ (completion work remaining)"]
FILTER --> STATUS_CHECK{"Status < 4?"}
STATUS_CHECK -->|Yes| POLL["Call Randox GetOrderStatus"]
POLL --> UPDATE_STATUS["Update case status in Table"]
STATUS_CHECK -->|"No - Completed"| RESULTS{"Results<br/>already pulled?"}
RESULTS -->|No| PULL["Pull results from Randox<br/>Store raw JSON in Blob"]
RESULTS -->|Yes| NEXT
PULL --> NEXT{"Panel type?"}
NEXT -->|"Standard blood panel"| ODX_FLOW["Submit HL7 to OptimalDX<br/>Generate FHR PDF<br/>Email via CRM"]
NEXT -->|"PDF-only panel"| RANDOX_PDF["Fetch Randox PDF<br/>Email via CRM"]
ODX_FLOW --> CLEANUP["Delete cached TOF blob<br/>Update case timestamps"]
RANDOX_PDF --> CLEANUP
Automation Configuration Flags
| Flag |
Default |
Purpose |
RandoxStatusPollEnabled |
true |
Enable/disable the hourly poller |
AutoSubmitOdxOnRandoxResultsPulled |
true |
Auto-submit results to OptimalDX when Randox completes |
AutoEmailOdxPatientReportOnRandoxCompleted |
false |
Auto-email patient report after Randox completes |
AutoSubmitOdxOnRandoxResultsPulledIncludeManualLinks |
false |
Include manually-linked orders in auto-submit |
AutoEmailOdxPatientReportOnRandoxCompletedIncludeManualLinks |
false |
Include manually-linked orders in auto-email |
9. Authentication & Security
flowchart LR
subgraph "User Auth"
USER["Staff User"] -->|"Entra ID login"| SWA["Static Web App<br/>(built-in auth)"]
SWA -->|"x-ms-client-principal<br/>(roles: How_Admin/Staff/ReadOnly)"| FUNC["Functions API"]
end
subgraph "Service Auth"
FUNC -->|"DefaultAzureCredential<br/>(Managed Identity)"| KV["Key Vault"]
FUNC -->|"DefaultAzureCredential"| STORAGE["Storage Account"]
FUNC -->|"Bearer token<br/>(OAuth2 client credentials)"| RANDOX["Randox"]
FUNC -->|"apiKey header"| ODX["OptimalDX"]
FUNC -->|"Bearer token<br/>(OAuth2 refresh / PIT)"| GHL["GoHighLevel"]
end
subgraph "CI/CD Auth"
GHA["GitHub Actions"] -->|"OIDC federation<br/>(Entra app registration)"| AZURE["Azure (deploy)"]
end
Role Matrix
| Role |
Portal Access |
API /api/* |
API /api/system/* |
API /api/diag/* |
How_ReadOnly |
✅ View only |
✅ Read |
❌ |
❌ |
How_Staff |
✅ Full |
✅ Read/Write |
✅ |
✅ |
How_Admin |
✅ Full |
✅ Read/Write |
✅ |
✅ |
anonymous |
❌ |
/api/health only |
❌ |
❌ |
10. CI/CD Pipeline
flowchart LR
RELEASE["GitHub Release<br/>(published)"] --> PROD_API["deploy-api-prod.yml"]
RELEASE --> PROD_PORTAL["deploy-portal-prod.yml"]
RELEASE --> DOCS["deploy-docs.yml<br/>(if docs/** changed)"]
PROD_API -->|"OIDC login"| PROD_FUNC["how-prod-api-func"]
PROD_PORTAL -->|"SWA deploy token"| PROD_SWA["how-prod-portal"]
DOCS -->|"SWA deploy token"| DOCS_SWA["how-prod-docs"]
style PROD_FUNC fill:#c8e6c9
style PROD_SWA fill:#c8e6c9
style DOCS_SWA fill:#c8e6c9
| Trigger |
Workflow |
Method |
| API deploy |
GitHub Release published |
OIDC (Entra federation, tag-scoped) |
| Portal deploy |
GitHub Release published |
SWA deploy token |
| Docs deploy |
Push to release/* branches |
SWA deploy token |