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 recordsGreen 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 deniedSensitivity 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 version4.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 noise4.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 openemr4.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)
- Create a test patient — fill demographics: name, address, phone, DOB, emergency contact
- Add custom fields (Admin → Forms → Layouts):
dietary_prefs(text) — dietary restrictions/preferencessms_opt_in(checkbox) — SMS consentmeal_plan_type(dropdown) — regular, diabetic, renal, etc.referral_source(text) — where the referral came from
- Create an appointment — use Calendar, create “Meal Delivery” category
- Assign to a group — test Patient Groups for program enrollment
- Add a document — upload a scanned intake form
- 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)
- Create Red Zone role (Admin → ACL → Groups):
- Clone “Physician” group → name it “CNP RN”
- Verify: sees all demographics + all clinical
- 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, allencounters/*
- Create White Zone role:
- Clone “Accounting” group → name it “CNP Reporting”
- Grant:
acct/rep(wsome),acct/rep_a(wsome) only
- Create test users for each role
- 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
- Test sensitivity tier: Mark one patient record as
highsensitivity. 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)
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"Fetch patients (Green Zone data):
curl -k -H "Authorization: Bearer $TOKEN" \ https://localhost:9380/apis/default/fhir/PatientFetch appointments:
curl -k -H "Authorization: Bearer $TOKEN" \ https://localhost:9380/apis/default/fhir/AppointmentTest 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"]}]}'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:
- Single system replaces CaseWorthy for CNP’s scope
- No external ETL extractor — ACL handles Red/Green/White partitioning internally
- FHIR API provides programmatic access for SMS bridge, delivery manifests, billing exports
- Patient portal gives participants self-service intake forms
- Calendar becomes the delivery scheduling backbone
- Billing module tracks grant line items
- 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
- OpenEMR Docker images: https://hub.docker.com/r/openemr/openemr
- OpenEMR ACL documentation:
src/Common/Acl/AclMain.php(in repo) - FHIR API routes:
apis/routes/_rest_routes_fhir_r4_us_core_3_1_0.inc.php - phpGACL admin UI:
/gacl/admin/acl_admin.php(after login) - OpenEMR wiki: https://www.open-emr.org/wiki/