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
Web App
How traffic flows
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.
- 1Sign in to medhook.dev and create a license key under Dashboard → API Keys.
- 2Clone the engine repo, copy
.env.exampleto.env, and fill in the three required secrets. - 3Run
docker compose up -d, hit/api/health, and open the dashboard.
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:3000Engine 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.
# 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.
| Method | Endpoint |
|---|---|
| POST | /api/auth/token |
Workflows
CRUD on workflow definitions. Workflows are DAGs of nodes and edges with a single trigger config.
| Method | Endpoint |
|---|---|
| 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.
| Method | Endpoint |
|---|---|
| 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.
| Method | Endpoint |
|---|---|
| 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.
| Method | Endpoint |
|---|---|
| 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.
| Method | Endpoint |
|---|---|
| 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.
| Method | Endpoint |
|---|---|
| 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.
| Method | Endpoint |
|---|---|
| GET | /api/health |
| GET | /api/metrics |
| POST | /api/batch |
| POST | /api/graphql |
| GET | /api/audit-log |
Example: create an adapter profile
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.
| Adapter | Type |
|---|---|
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 type | Description |
|---|---|
| adapter | Call an integration adapter (read or write). |
| transform | Apply field mappings, code tables, or custom JavaScript. |
| condition | If / else branching based on data expressions. |
| parallel | Execute multiple branches concurrently. |
| loop | Iterate over arrays with a max-iteration guard. |
| wait | Pause for a webhook callback or timer. |
| trigger | Spawn another workflow as a sub-process. |
Trigger types (8)
| Trigger type | Description |
|---|---|
| manual | User-initiated execution from UI or API. |
| webhook | Inbound HTTP POST with optional HMAC validation. |
| schedule | Simple cron expression (UTC). |
| cron | Advanced cron with timezone support. |
| polling | Interval-based adapter polling. |
| mllp | HL7v2 message via MLLP TCP listener (port 2575). |
| fhir-subscription | FHIR R4 subscription notification (REST hook). |
| x12 | X12 EDI transaction ingestion via SFTP, webhook, or upload. |
Transform types (9)
| Transform type | Description |
|---|---|
| field-mapping | Visual drag-and-drop field mapper with dot-notation paths. |
| javascript | Sandboxed custom JS (isolated-vm, 128 MB heap, 5 s timeout). |
| code-table | Lookup / translation tables. |
| hl7v2-to-fhir | Built-in HL7v2 to FHIR R4 conversion (ADT, ORM, ORU). |
| fhir-to-hl7v2 | Built-in FHIR R4 to HL7v2 conversion. |
| x12-parse | Parse X12 EDI to JSON. |
| x12-generate | Generate X12 EDI from JSON. |
| cda-parse | Parse HL7 CDA / C-CDA XML to JSON. |
| template | Handlebars-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.
| Variable | Required |
|---|---|
| DATABASE_URL | required |
| REDIS_URL | required |
| ENCRYPTION_MASTER_KEY | required |
| ENGINE_AUTH_SECRET | required |
| LICENSE_KEY | required |
| WEB_APP_URL | optional |
| SKIP_LICENSE_VALIDATION | optional |
| NODE_ENV | optional |
| LOG_LEVEL | optional |
| MAX_CONCURRENT_WORKFLOWS | optional |
| AUDIT_LOG_RETENTION_DAYS | optional |
| SESSION_DATA_TTL_HOURS | optional |
| EXECUTION_LOG_TTL_DAYS | optional |
| ALLOW_PRIVATE_URLS | optional |
| MEDHOOK_PHI_ENFORCEMENT | optional |
| MEDPLUM_BASE_URL | optional |
| MEDPLUM_CLIENT_ID | optional |
| MEDPLUM_CLIENT_SECRET | optional |
| ANTHROPIC_API_KEY | optional |
| GRAFANA_ADMIN_USER | optional |
| GRAFANA_ADMIN_PASSWORD | optional |
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
ENCRYPTION_MASTER_KEY.JWT auth (2 h expiry)
ENGINE_AUTH_SECRET. Validated by middleware on every protected route. Rate-limited at 5 issuance attempts per minute per IP.MFA / TOTP
TOTP_ENCRYPTION_KEY.IP allowlist
Sandboxed JS execution
Audit logging
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.
# from the engine repo root
docker compose up -d
# tail logs
docker compose logs -f engine
# stop
docker compose downCloud (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.
engine/terraform/awsengine/terraform/azureengine/terraform/gcpcd engine/terraform/aws
terraform init
terraform apply
# Same pattern for Azure and GCP:
# engine/terraform/azure
# engine/terraform/gcpKubernetes (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.