Evaluation Plan

OpenEMR Evaluation Plan for CNP

Goal: Deploy OpenEMR locally via Docker and evaluate it as an in-system zone-partitioned platform (Red/Green/White) that could replace CaseWorthy + external ETL extractor.

Key insight: OpenEMR’s phpGACL-based permissions system is granular enough to partition clinical data from operational data within the same system, eliminating the need for an external Red→Green extractor boundary. Different roles see different subsets of the same patient record. This is architecturally simpler and more auditable than building separate pipelines.


1. Why OpenEMR Is Worth Evaluating

Requirement OpenEMR Coverage Notes
Patient demographics ✅ Native Name, address, phone, DOB, contacts
Appointment scheduling ✅ Native calendar Map to meal delivery slots
REST API ✅ 33 FHIR R4 resources Read-mostly; Patient/Org/Practitioner writable
Granular ACL / roles ✅ phpGACL section/object Clinical vs demographic separation
Document storage ✅ Native + CouchDB CCDA, scanned docs
Billing / accounting ✅ Native Could map to grant billing
Reporting ✅ Built-in reports Customizable
Patient portal ✅ Native Self-service forms
HL7 support ✅ Lab/immunization Limited to clinical HL7
SMS integration ⚠️ Not native Would need custom module or external bridge
Program enrollment ❌ No native concept Map to patient groups / custom fields
Referral management ⚠️ Clinical referrals only Not social-services referral workflow
Case management ❌ Not designed for it Would need significant customization

Verdict: Not a drop-in CaseWorthy replacement, but the ACL partitioning alone is compelling enough to evaluate. If the gaps are manageable with light customization, OpenEMR could work.


2. Zone-to-ACL Mapping

OpenEMR’s ACL uses three-tier section / object / access_level tuples. Here’s the CNP zone mapping:

Red Zone — RN Full Access

// All sections, all objects, write access
// Essentially the "Physician" default group
patients/demo    → write
patients/med     → write   // diagnoses, history
patients/notes   → write   // clinical notes
patients/docs    → write   // documents
patients/appt    → write   // appointments
patients/sign    → write   // lab results
patients/trans   → write   // transactions
encounters/notes_a → write // encounter notes
encounters/auth_a → write
encounters/coding_a → write
acct/bill        → write   // billing
acct/eob         → write
admin/*          → write   // administration
sensitivities/high → write // HIV, MH, SUD records

Green Zone — Operations Staff (PII only)

// Demographics & scheduling ONLY — no clinical
patients/demo    → write   // name, address, phone, DOB
patients/appt    → write   // scheduling / delivery calendar
patients/docs    → addonly // upload delivery forms
acct/bill        → write   // billing (grant tracking)
acct/rep         → write   // reports
patients/med     → DENIED  // NO medical history
patients/notes   → DENIED  // NO clinical notes
patients/sign    → DENIED  // NO lab results
encounters/*     → DENIED  // NO encounter access
sensitivities/*  → DENIED  // NO sensitive data
admin/*          → DENIED  // NO admin functions (except calendar)

White Zone — Reporting / Analytics (de-identified)

// Reports only — no individual patient drill-down
acct/rep         → wsome   // reports (aggregate)
acct/rep_a       → wsome   // advanced reports
patients/demo    → DENIED  // NO individual demographics
patients/med     → DENIED
// Everything else denied

Sensitivity Tiers (bonus)

OpenEMR supports sensitivities/normal and sensitivities/high tiers layered on top of section ACLs. This means PHI-level data can be flagged at the record level — even if a Green Zone user somehow got patients/med access, sensitivity-gated records would still be hidden.


3. OpenEMR Concepts → CNP Concepts Mapping

CNP Concept OpenEMR Mapping How
Participant Patient record Standard patient demographics
Meal plan Appointment type (custom) Create “Meal Delivery” appointment category
Delivery schedule Calendar / Appointments Weekly recurring appointments
Program enrollment Patient Groups or Custom List Assign patients to “CNP Program” group
Delivery route Facility or User assignment Assign patients to delivery driver (provider)
Referral (inbound) Patient intake form Portal form or custom module
Referral (outbound) Transaction/Referral form Clinical referral form (limited)
Wellness check Encounter (non-clinical type) Custom encounter form for check-ins
Billing/claims Billing module Fee sheet for grant line items
SMS opt-in/out Custom patient field Patient-level custom attribute
Dietary preferences Custom patient field Patient-level custom attribute
Emergency contact Patient Contacts Built-in contact fields

4. Docker Deployment Plan

4.1 Prerequisites

# macOS
brew install docker  # or Docker Desktop

# Verify
docker --version
docker compose version

4.2 Clone and Configure

cd ~/Projects/CNP
mkdir -p openemr-eval
cd openemr-eval

# Clone OpenEMR
git clone https://github.com/openemr/openemr.git
cd openemr

# We'll use the dev-easy-light compose (just MySQL + OpenEMR + phpMyAdmin)
# It's the simplest and avoids CouchDB, LDAP, Selenium noise

4.3 Custom Docker Compose

The upstream compose files expect to run from within the OpenEMR repo. We’ll create our own simplified version that works standalone:

# ~/Projects/CNP/openemr-eval/docker-compose.yml
services:
  mysql:
    restart: always
    image: mariadb:11.8
    command: ['mariadbd', '--character-set-server=utf8mb4']
    ports:
      - "8330:3306"
    volumes:
      - openemr_db:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: root

  openemr:
    restart: always
    image: openemr/openemr:flex
    ports:
      - "8380:80"
      - "9380:443"
    volumes:
      - openemr_sites:/var/www/localhost/htdocs/openemr/sites:rw
      - openemr_logs:/var/log
    environment:
      MYSQL_HOST: mysql
      MYSQL_ROOT_PASS: root
      MYSQL_USER: openemr
      MYSQL_PASS: openemr
      OE_USER: admin
      OE_PASS: pass
      # Enable APIs
      OPENEMR_SETTING_site_addr_oath: 'https://localhost:9380'
      OPENEMR_SETTING_oauth_password_grant: 3
      OPENEMR_SETTING_rest_system_scopes_api: 1
      OPENEMR_SETTING_rest_api: 1
      OPENEMR_SETTING_rest_fhir_api: 1
      OPENEMR_SETTING_rest_portal_api: 1
    depends_on:
      mysql:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "--fail", "--insecure", "--silent", "https://localhost/meta/health/readyz"]
      start_period: 3m
      interval: 1m
      timeout: 5s
      retries: 3

  phpmyadmin:
    restart: always
    image: phpmyadmin:5.2
    ports:
      - "8340:80"
    environment:
      PMA_HOSTS: mysql
    depends_on:
      mysql:
        condition: service_healthy

volumes:
  openemr_db:
  openemr_sites:
  openemr_logs:

4.4 Launch

cd ~/Projects/CNP/openemr-eval
docker compose up -d

# Wait for healthy (about 3 minutes first time)
docker compose ps

# Watch logs if needed
docker compose logs -f openemr

4.5 First Access

Service URL Credentials
OpenEMR https://localhost:9380 admin / pass
phpMyAdmin http://localhost:8340 root / root (server: mysql)
FHIR API https://localhost:9380/apis/default/fhir OAuth2 bearer token
FHIR metadata https://localhost:9380/apis/default/fhir/metadata Public

On first login, you’ll go through a brief setup wizard — accept defaults.

⚠️ Self-signed cert: Browser will warn. Click “Advanced → Proceed” (dev only).


5. Evaluation Steps (What to Test)

Phase 1: Can It Model CNP Data? (15 min)

  1. Create a test patient — fill demographics: name, address, phone, DOB, emergency contact
  2. Add custom fields (Admin → Forms → Layouts):
    • dietary_prefs (text) — dietary restrictions/preferences
    • sms_opt_in (checkbox) — SMS consent
    • meal_plan_type (dropdown) — regular, diabetic, renal, etc.
    • referral_source (text) — where the referral came from
  3. Create an appointment — use Calendar, create “Meal Delivery” category
  4. Assign to a group — test Patient Groups for program enrollment
  5. Add a document — upload a scanned intake form
  6. Record a non-clinical encounter — create a custom encounter form for wellness check-ins

Verdict questions: - Does the data model feel natural for meal delivery, or are we fighting clinical metaphors? - Can custom fields carry enough structured data without custom modules?

Phase 2: Can ACL Partition the Zones? (20 min)

  1. Create Red Zone role (Admin → ACL → Groups):
    • Clone “Physician” group → name it “CNP RN”
    • Verify: sees all demographics + all clinical
  2. Create Green Zone role:
    • Clone “Front Office” group → name it “CNP Operations”
    • Grant: patients/demo (write), patients/appt (write), acct/bill (write), acct/rep (write)
    • Deny: patients/med, patients/notes, patients/sign, all encounters/*
  3. Create White Zone role:
    • Clone “Accounting” group → name it “CNP Reporting”
    • Grant: acct/rep (wsome), acct/rep_a (wsome) only
  4. Create test users for each role
  5. Log in as each and verify:
    • RN sees clinical notes, diagnoses, lab results
    • Operations sees demographics + appointments + billing, but clicking “Medical History” = denied
    • Reporting sees reports but cannot open individual patient charts
  6. Test sensitivity tier: Mark one patient record as high sensitivity. Verify Green/White roles cannot see it even if ACL were misconfigured.

Verdict questions: - Is the ACL boundary clean enough for HIPAA compliance? - Can a Green Zone user accidentally see PHI through reports, search, or audit logs? - Are there edge cases (e.g., appointment notes that contain clinical info)?

Phase 3: Can the FHIR API Serve ETL Pipelines? (15 min)

  1. Get OAuth2 token:

    curl -k -X POST https://localhost:9380/apis/default/oauth2/token \
      -d "grant_type=password&username=admin&password=pass&client_id=openemr&scope=openid"
  2. Fetch patients (Green Zone data):

    curl -k -H "Authorization: Bearer $TOKEN" \
      https://localhost:9380/apis/default/fhir/Patient
  3. Fetch appointments:

    curl -k -H "Authorization: Bearer $TOKEN" \
      https://localhost:9380/apis/default/fhir/Appointment
  4. Test write operations:

    # Create patient via FHIR
    curl -k -X POST -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/fhir+json" \
      https://localhost:9380/apis/default/fhir/Patient \
      -d '{"resourceType":"Patient","name":[{"family":"Test","given":["Demo"]}]}'
  5. Verify ACL applies to API: Call FHIR endpoints with a Green Zone user’s token. Does the API respect ACL? (It should — OpenEMR ACL gates everything.)

Verdict questions: - Are the writable resources (Patient, Organization, Practitioner) enough? - ServiceRequest is read-only — can we work around this? - Does the FHIR API return PHI fields (diagnoses, notes) to a Green Zone token, or does ACL filter them?

Phase 4: Map the Missing Pieces (10 min)

Gap Workaround Viable?
Program enrollment Patient Groups or custom list
Meal plan tracking Appointment type + custom fields
SMS integration External bridge calling Twilio API
Social services referrals Custom encounter form or external module ⚠️
Delivery manifest generation SQL query or FHIR Appointment export
Case management workflow Not native — would need custom module

6. Go/No-Go Criteria

After Phase 1–4 evaluation, answer:

Question Required Answer
Can we model CNP’s data without fighting clinical metaphors? Yes
Is the ACL boundary HIPAA-sufficient for Red→Green separation? Yes, with no PHI leaks in Green
Does FHIR API provide enough write endpoints for ETL? Patient + Appointment write is enough
Are the missing pieces (SMS, referrals) bridgeable with <2 weeks dev? Yes
Is the system complexity justified vs. building a custom Django app? OpenEMR provides ACL, FHIR, portal, and calendar for free

If ≥4/5 are “Yes” → Proceed to production plan. If <4/5 → Fall back to CaseWorthy API confirmation or custom Django build.


7. What Success Looks Like

If OpenEMR passes evaluation:

  1. Single system replaces CaseWorthy for CNP’s scope
  2. No external ETL extractor — ACL handles Red/Green/White partitioning internally
  3. FHIR API provides programmatic access for SMS bridge, delivery manifests, billing exports
  4. Patient portal gives participants self-service intake forms
  5. Calendar becomes the delivery scheduling backbone
  6. Billing module tracks grant line items
  7. Provenance/audit comes from OpenEMR’s built-in logging (all ACL-gated)

The architecture simplifies from:

CaseWorthy → Extractor → Green DB → ETL pipelines → SMS/OneDrive
                                  ↘ Red DB (RN laptop)

To:

OpenEMR ──ACL──→ Red view (RN: full clinical)
        ├─ACL──→ Green view (Ops: demographics + scheduling + billing)
        ├─FHIR──→ ETL pipelines (SMS bridge, manifests)
        └─Reports→ White view (de-identified aggregates)

8. Script: Quick Deploy

Save as ~/Projects/CNP/openemr-eval/deploy.sh:

#!/usr/bin/env bash
set -euo pipefail

DIR="$(cd "$(dirname "$0")" && pwd)"
cd "$DIR"

echo "=== OpenEMR CNP Evaluation Deploy ==="

# Write docker-compose.yml if not present
if [ ! -f docker-compose.yml ]; then
  echo "Writing docker-compose.yml..."
  cat > docker-compose.yml <<'YAML'
services:
  mysql:
    restart: always
    image: mariadb:11.8
    command: ['mariadbd', '--character-set-server=utf8mb4']
    ports: ["8330:3306"]
    volumes: [openemr_db:/var/lib/mysql]
    environment: { MYSQL_ROOT_PASSWORD: root }

  openemr:
    restart: always
    image: openemr/openemr:flex
    ports: ["8380:80", "9380:443"]
    volumes:
      - openemr_sites:/var/www/localhost/htdocs/openemr/sites:rw
      - openemr_logs:/var/log
    environment:
      MYSQL_HOST: mysql
      MYSQL_ROOT_PASS: root
      MYSQL_USER: openemr
      MYSQL_PASS: openemr
      OE_USER: admin
      OE_PASS: pass
      OPENEMR_SETTING_site_addr_oath: 'https://localhost:9380'
      OPENEMR_SETTING_oauth_password_grant: 3
      OPENEMR_SETTING_rest_system_scopes_api: 1
      OPENEMR_SETTING_rest_api: 1
      OPENEMR_SETTING_rest_fhir_api: 1
      OPENEMR_SETTING_rest_portal_api: 1
    depends_on:
      mysql:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "--fail", "--insecure", "--silent", "https://localhost/meta/health/readyz"]
      start_period: 3m
      interval: 1m
      timeout: 5s
      retries: 3

  phpmyadmin:
    restart: always
    image: phpmyadmin:5.2
    ports: ["8340:80"]
    environment: { PMA_HOSTS: mysql }
    depends_on: { mysql: { condition: service_healthy } }

volumes:
  openemr_db:
  openemr_sites:
  openemr_logs:
YAML
fi

echo "Starting containers..."
docker compose up -d

echo ""
echo "Waiting for OpenEMR to be healthy (up to 3 minutes)..."
for i in $(seq 1 36); do
  if curl -sk https://localhost:9380/meta/health/readyz 2>/dev/null | grep -q ok; then
    echo "✓ OpenEMR is ready!"
    break
  fi
  sleep 5
  echo -n "."
done

echo ""
echo "=== Deployment Complete ==="
echo "OpenEMR:    https://localhost:9380  (admin / pass)"
echo "phpMyAdmin: http://localhost:8340   (root / root, server: mysql)"
echo "FHIR API:   https://localhost:9380/apis/default/fhir/metadata"
echo ""
echo "Next: Open https://localhost:9380, accept self-signed cert,"
echo "      complete setup wizard, then follow evaluation steps in"
echo "      docs/OpenEMR-Evaluation-Plan.md"

9. References

← Back to CNP Portal