Documentation

MedHook Documentation

Self-hosted healthcare data integration. Deploy the engine via Docker, build DAG-based workflows, connect EHRs and labs through typed adapters, and ship integrations in minutes — not months.

Overview

MedHook is a self-hosted healthcare data integration platform. It moves data between EHRs, labs, payers, and internal systems using visual DAG workflows, typed adapters, and AI-assisted field mapping. PHI never leaves your infrastructure.

The platform ships as three independently deployable components:

Engine

Self-hosted runtime that owns all PHI, executes workflows, and runs adapters — Next.js 14 + Postgres + Redis in an 8-service Docker Compose stack.

Web App

SaaS control plane on medhook.dev for accounts, license keys, billing, and aggregate usage analytics — no PHI ever crosses this boundary.

How traffic flows

EHR / Lab / PayerAdapterTransformAdapterDestination

Quick Start

A single Docker Compose command brings up the full engine stack: Next.js API, Postgres, Redis, Medplum, MLLP listener, Prometheus, and Grafana. Total time, with Docker already installed: under five minutes.

  1. 1
    Sign in to medhook.dev and create a license key under Dashboard → API Keys.
  2. 2
    Clone the engine repo, copy .env.example to .env, and fill in the three required secrets.
  3. 3
    Run docker compose up -d, hit /api/health, and open the dashboard.
bash
git clone https://github.com/medhook/engine.git
cd engine
cp .env.example .env

# Fill in the three required secrets:
#   ENCRYPTION_MASTER_KEY  (openssl rand -hex 32)
#   ENGINE_AUTH_SECRET     (openssl rand -hex 64)
#   LICENSE_KEY            (medhook.dev → Dashboard → API Keys)

docker compose up -d

# Verify
curl http://localhost:3000/api/health
# → { "status": "healthy" }

open http://localhost:3000

Engine API

Every action in the engine is reachable through a REST API. JWT bearer tokens are obtained from /api/auth/token and expire after 2 hours. Pass the token in the Authorization: Bearer header on every other call.

auth flow
# Exchange the engine auth secret for a 2-hour JWT
TOKEN=$(curl -s -X POST http://localhost:3000/api/auth/token \
  -H "Content-Type: application/json" \
  -d '{
    "secret": "'"$ENGINE_AUTH_SECRET"'",
    "licenseKey": "'"$LICENSE_KEY"'"
  }' | jq -r .token)

# Use it
curl http://localhost:3000/api/workflows \
  -H "Authorization: Bearer $TOKEN"

Authentication

Exchange your engine auth secret for a short-lived JWT (2 hour expiry). Required for every other API call.

MethodEndpoint
POST/api/auth/token

Workflows

CRUD on workflow definitions. Workflows are DAGs of nodes and edges with a single trigger config.

MethodEndpoint
GET/api/workflows
POST/api/workflows
GET/api/workflows/:id
PUT/api/workflows/:id
DELETE/api/workflows/:id
GET/api/workflows/templates
POST/api/workflows/:id/publish

Executions

Trigger runs, fetch status, stream live progress, cancel, and replay.

MethodEndpoint
POST/api/workflows/:id/execute
GET/api/executions
GET/api/executions/:id
GET/api/executions/:id/logs
GET/api/executions/:id/stream
POST/api/executions/:id/cancel
POST/api/executions/:id/replay

Adapters & Profiles

List adapter types and manage encrypted adapter profiles (credential bundles). Credentials are AES-256-GCM encrypted at rest.

MethodEndpoint
GET/api/adapters
GET/api/adapters/:type
GET/api/adapter-profiles
POST/api/adapter-profiles
PUT/api/adapter-profiles/:id
DELETE/api/adapter-profiles/:id
POST/api/adapter-profiles/:id/test

Triggers

Inbound trigger receivers — webhooks, FHIR subscriptions, parsed HL7v2 from MLLP, and X12 EDI transactions.

MethodEndpoint
POST/api/triggers/webhook/:webhookId
POST/api/triggers/cron
POST/api/triggers/mllp
POST/api/triggers/x12
POST/api/triggers/fhir-subscription/:triggerId

Mapping & Code Tables

Field mapping sets and code translation tables that power the transform node types.

MethodEndpoint
GET/api/mapping-sets
POST/api/mapping-sets
PUT/api/mapping-sets/:id
DELETE/api/mapping-sets/:id
GET/api/code-tables
POST/api/code-tables
PUT/api/code-tables/:id
DELETE/api/code-tables/:id

AI

Claude-powered assistance for field mapping suggestions and condition expression generation.

MethodEndpoint
GET/api/ai/mapping-suggest
POST/api/ai/mapping-suggest
POST/api/ai/condition-suggest

System

Health checks, Prometheus metrics, batch helpers, GraphQL, and audit log query.

MethodEndpoint
GET/api/health
GET/api/metrics
POST/api/batch
POST/api/graphql
GET/api/audit-log

Example: create an adapter profile

POST /api/adapter-profiles
curl -X POST http://localhost:3000/api/adapter-profiles \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Epic FHIR Production",
    "adapterType": "fhir-rest",
    "config": {
      "baseUrl": "https://fhir.epic.com/api/FHIR/R4",
      "authType": "bearer",
      "credentials": { "token": "your-bearer-token" }
    }
  }'

The full reference (every route, request schema, and response shape) lives at docs/API.md in the engine repository.

Adapters

Adapters are typed connectors — config validated by Zod, credentials encrypted with AES-256-GCM, connection-tested before use. Fourteen adapters ship in the box, covering EHRs, FHIR servers, REST APIs, file transfer, HL7v2, X12, and CDA.

AdapterType
FHIR REST
fhir-rest
Epic FHIR
epic-fhir
Athena FHIR
athena-fhir
Oracle FHIR
oracle-fhir
Allscripts FHIR
allscripts-fhir
NextGen FHIR
nextgen-fhir
Medplum
medplum
FHIR Bulk
fhir-bulk
Generic REST
generic-rest
Webhook
webhook
SFTP
sftp
MLLP Outbound
mllp
X12 EDI
x12
CDA / C-CDA
cda

Need a connector that isn't listed? The @medhook/adapter-sdk package lets you build out-of-tree adapters that plug into the engine without forking it.

Workflow Concepts

Every integration in MedHook is a directed acyclic graph (DAG): a trigger fires, nodes execute, and edges connect them. Branching, parallel execution, and loops are all expressed as nodes — there is no separate orchestration layer.

Node types (7)

Node typeDescription
adapterCall an integration adapter (read or write).
transformApply field mappings, code tables, or custom JavaScript.
conditionIf / else branching based on data expressions.
parallelExecute multiple branches concurrently.
loopIterate over arrays with a max-iteration guard.
waitPause for a webhook callback or timer.
triggerSpawn another workflow as a sub-process.

Trigger types (8)

Trigger typeDescription
manualUser-initiated execution from UI or API.
webhookInbound HTTP POST with optional HMAC validation.
scheduleSimple cron expression (UTC).
cronAdvanced cron with timezone support.
pollingInterval-based adapter polling.
mllpHL7v2 message via MLLP TCP listener (port 2575).
fhir-subscriptionFHIR R4 subscription notification (REST hook).
x12X12 EDI transaction ingestion via SFTP, webhook, or upload.

Transform types (9)

Transform typeDescription
field-mappingVisual drag-and-drop field mapper with dot-notation paths.
javascriptSandboxed custom JS (isolated-vm, 128 MB heap, 5 s timeout).
code-tableLookup / translation tables.
hl7v2-to-fhirBuilt-in HL7v2 to FHIR R4 conversion (ADT, ORM, ORU).
fhir-to-hl7v2Built-in FHIR R4 to HL7v2 conversion.
x12-parseParse X12 EDI to JSON.
x12-generateGenerate X12 EDI from JSON.
cda-parseParse HL7 CDA / C-CDA XML to JSON.
templateHandlebars-style template rendering.

Environment Variables

Configuration for the engine lives in engine/.env. Three secrets must be set before first boot — everything else has a working default for local Docker Compose.

VariableRequired
DATABASE_URLrequired
REDIS_URLrequired
ENCRYPTION_MASTER_KEYrequired
ENGINE_AUTH_SECRETrequired
LICENSE_KEYrequired
WEB_APP_URLoptional
SKIP_LICENSE_VALIDATIONoptional
NODE_ENVoptional
LOG_LEVELoptional
MAX_CONCURRENT_WORKFLOWSoptional
AUDIT_LOG_RETENTION_DAYSoptional
SESSION_DATA_TTL_HOURSoptional
EXECUTION_LOG_TTL_DAYSoptional
ALLOW_PRIVATE_URLSoptional
MEDHOOK_PHI_ENFORCEMENToptional
MEDPLUM_BASE_URLoptional
MEDPLUM_CLIENT_IDoptional
MEDPLUM_CLIENT_SECREToptional
ANTHROPIC_API_KEYoptional
GRAFANA_ADMIN_USERoptional
GRAFANA_ADMIN_PASSWORDoptional

For production deployments, store the three required secrets in your platform's secrets manager (AWS Secrets Manager, Azure Key Vault, GCP Secret Manager) instead of a plaintext .env file.

Security

Healthcare-grade defaults. PHI never leaves your infrastructure: medhook.dev only receives HMAC-signed aggregate analytics — never workflow data.

AES-256-GCM at rest

All adapter credentials are encrypted with AES-256-GCM using PBKDF2 key derivation (100k iterations), unique salt and IV per record. Master key from ENCRYPTION_MASTER_KEY.

JWT auth (2 h expiry)

Bearer tokens signed with ENGINE_AUTH_SECRET. Validated by middleware on every protected route. Rate-limited at 5 issuance attempts per minute per IP.

MFA / TOTP

The web app enforces TOTP-based MFA on the SaaS portal with recovery codes. Secrets are encrypted at rest using PBKDF2 + AES-256-GCM under TOTP_ENCRYPTION_KEY.

IP allowlist

CIDR-based allowlist enforced at the engine middleware layer. IPv4-mapped IPv6 addresses are normalised; the caller's resolved IP is echoed by the settings API to prevent lockout.

Sandboxed JS execution

Custom JavaScript transforms run inside isolated-vm with a 128 MB memory cap and 5 s timeout. Inputs are read-only frozen objects; no Node.js APIs, no network, no file system.

Audit logging

Every CRUD event on workflows, adapter profiles, triggers, mappings, and licenses is written to Postgres with 90-day default retention. Queryable via GET /api/audit-log with CSV export.

Disclosure: see /security for our vulnerability reporting policy and the latest audit summary.

Deployment

MedHook is fully self-hosted. The fastest path is Docker Compose for local and single-server deployments. Terraform templates exist for AWS, Azure, and GCP. A Helm chart and air-gap install are available for regulated environments.

Docker Compose

Spin up the entire 8-service stack — engine, Postgres, Redis, Medplum, MLLP listener, Prometheus, Grafana, optional IPSec VPN — with one command.

bash
# from the engine repo root
docker compose up -d

# tail logs
docker compose logs -f engine

# stop
docker compose down

Cloud (AWS / Azure / GCP)

Each cloud has a self-contained Terraform module that provisions networking, secrets, the container service, managed Postgres, and managed Redis. Pick the closest match to your existing infrastructure.

AWS Fargate
engine/terraform/aws
Azure Container Instances
engine/terraform/azure
GCP Cloud Run
engine/terraform/gcp
bash — AWS example
cd engine/terraform/aws
terraform init
terraform apply

# Same pattern for Azure and GCP:
#   engine/terraform/azure
#   engine/terraform/gcp

Kubernetes (Helm)

A Helm chart is published at charts/medhook-engine in the engine repository. It packages the same eight services as the Docker Compose stack as Kubernetes resources, with values for ingress, persistent volumes, secrets, and HPA. Multi-region warm-standby topologies are supported via the engine/terraform/multi-region modules.

On-prem / air-gapped

Any Docker-capable host works. For offline license activation and private image registries — set ANALYTICS_SYNC_ENABLED=false and pre-validate the license once, then cache indefinitely. For full air-gap guidance, contact us.

Ready to ship?

Get a license key and run docker compose up -d. Your first integration can be live in minutes.