University Project · EMR + FHIR R4 Platform

OpenSeva

Production-grade Electronic Medical Records system backed by a Rust FHIR R4 server, PostgreSQL, AI-driven dataset generation, and Claude AI integration via the Model Context Protocol.

FHIR R4 React + Vite Rust HFS PostgreSQL 16 Node.js Auth MCP (Claude AI) Docker Compose

Platform Overview

6 Services + HIE Layer

OpenSeva is a monorepo of six integrated services. Each service has a clearly defined responsibility and communicates over HTTP using FHIR R4 and standard OAuth2/JWT. On top of these services, OpenSeva now simulates a Health Information Exchange — multiple EMRs feeding one shared record.

EMR vs EHR vs HIE. An EMR is one clinic's chart (the six services below run a single clinic). An EHR is a record designed to follow the patient across providers. A Health Information Exchange (HIE) is the layer that moves records between organisations and assembles one longitudinal record. The new HIE Simulation Layer (last card) turns OpenSeva into a network of EMRs — see the HIE Simulation section for the full walkthrough.

EMR Frontend

React/Vite clinical application for patient registration, encounters, appointments, prescriptions, and scheduling.


:3000 :3100 Docker

Auth Server

Node.js OAuth2 server. Issues RS256 JWTs. Exposes /.well-known/jwks.json for FHIR JWT validation. MCP PIN API built-in.


:8088 :8188 Docker

FHIR Server (HFS)

Rust-based FHIR R4 server. SQLite for local dev, PostgreSQL for production. Validates JWTs via JWKS in Docker mode.


:8080 :8180 Docker

MCP Server

Claude AI integration via Model Context Protocol (stdio transport). Role-gated tools for querying patients, encounters, medications, and appointments.


stdio

Dataset Worker

Server-side LLM caller. Generates synthetic FHIR bundles via OpenAI-compatible API, validates them, uploads to HFS with auth, and persists job history.


:8091 :8191 Docker

FHIR Admin UI

Static HTML admin portal. Tabs: Search, Create/Edit, Bulk Upload, Dataset Studio (AI generation), Job History. Admin-only in Docker mode.


:8090 :8181 Docker

HIE Simulation Layer New

Multiple simulated EMRs (Clinic, Hospital, Lab, Pharmacy) feed one shared FHIR store, each resource tagged by source. The foundation for cross-org patient matching and a unified longitudinal record.


seed-hie.sh Track E

Architecture Diagram

Both Stacks

Local Stack (run.sh)

[ Browser ] │ ├──OAuth2 implicit──►[ Auth :8088 ] │ │ RS256 JWT │◄──token in URL hash── │ │ ▼ ▼ [ SQLite fhir.db ] [ EMR :3000 ] │ All FHIR calls │ (no auth required │ in SQLite mode) ▼ [ HFS FHIR :8080 ] ← open │ ▼ [ SQLite fhir.db ] [ Dataset Worker :8091 ] │ calls LLM (optional) │ uploads to FHIR [ MCP Server ] stdio ► Claude │ PIN exchange ► Auth :8088 │ FHIR queries ► HFS :8080 [ Portal :8090 ] ► FHIR Admin UI

Docker Stack (Compose)

[ Browser ] │ ├──OAuth2──►[ Auth :8188 ] │ │ RS256 JWT (stable key) │ │ bcrypt users in PG │ ▼ │ [ PostgreSQL :5432 ] │ openseva_auth │ ▼ [ EMR :3100 ] │ Authorization: Bearer [ HFS FHIR :8180 ] ← JWT required │ Validates RS256 via JWKS │ from http://auth:8088 ▼ [ PostgreSQL :5432 ] openseva_fhir [ Dataset Worker :8191 ] │ auth required (admin only) │ job history: volume mount [ FHIR Admin :8181 ] nginx [ Swagger UI :8182 ]
Key difference: In Docker, every FHIR write requires a valid RS256 JWT issued by the Auth Server. In local run.sh mode, FHIR accepts all requests (JWT auth explicitly disabled for dev convenience).

HIE Simulation Layer New

A data layer on top of either stack — there is no new server. ./seed-hie.sh loads four tenant EMRs into the same FHIR store, each resource tagged by its source org. Tenant scoping works today; the exchange / matching view is the Track E roadmap, not yet built.

[ Clinic A ] [ Hospital B ] [ Lab C ] [ Pharmacy D ] ← 4 tenant Organizations \ | | / \____________|_______________|____________/ ▼ every resource: meta.tag = source org [ Shared HFS FHIR store ] ✓ works today │ scope a tenant: ?organization=Organization/org-X ▼ [ HIE Exchange view ] ✓ basic viewer built deterministic match · merge cross-org record (fuzzy MPI + consent = roadmap)
Honest status: Tenant data is seeded (tested), and a working HIE Exchange viewer now does deterministic cross-org matching + a merged record at portal/hie-exchange.html. Still roadmap: probabilistic/fuzzy MPI scoring, consent enforcement, and deduplication. Also: HFS does not index _tag as a filter — tenant scoping uses ?organization=.

Credentials & Service URLs

Same credentials, both stacks
Admin
admin@emr.local
admin123
  • Full EMR access
  • FHIR Admin + Dataset Studio
  • User management APIs
  • MCP: all tools

FHIR: Organization/org-local

Doctor / Provider
doctor@emr.local
doctor123
  • Patient list & charts
  • Encounter notes
  • Prescriptions & observations
  • MCP: patient/encounter tools

FHIR: Practitioner/pract-doctor

Receptionist
reception@emr.local
reception123
  • Appointment scheduling
  • Patient registration
  • Check-in workflows
  • MCP: scheduling tools

FHIR: Practitioner/pract-receptionist

Patient
patient@emr.local
patient123
  • Own records only
  • Medications & lab results
  • Appointments
  • MCP: self-service tools

FHIR: Patient/patient-local

HIE tenants have no separate logins. The four EMR tenants (Clinic A, Hospital B, Lab C, Pharmacy D) are seeded data, not user accounts — load them with ./seed-hie.sh and query them with the tenant-scoped URLs below. You still sign in with the four accounts above.

Service URLs — Quick Reference

Service Local (run.sh) · SQLite Docker · PostgreSQL
EMR Login localhost:3000/signin localhost:3100/signin
FHIR Admin UI localhost:8090/fhir-admin.html localhost:8181/
Auth Health localhost:8088/health localhost:8188/health
Auth JWKS localhost:8088/.well-known/jwks.json localhost:8188/.well-known/jwks.json
FHIR Health localhost:8080/health localhost:8180/health
FHIR Metadata localhost:8080/metadata localhost:8180/metadata
Dataset Worker localhost:8091/health localhost:8191/health
MCP Connect localhost:3000/mcp-connect use local stack for MCP
Swagger UI localhost:8182/
Portal / Nav localhost:8090/
HIE — Hospital B patients …8080/Patient?organization=…org-hospital-b …8180/Patient?organization=…org-hospital-b
HIE — Priya's diabetes (Clinic A only) …8080/Condition?subject=Patient/hie-a-priya …8180/Condition?subject=Patient/hie-a-priya

Step 1 — Verify Local Stack is Running

Local · run.sh

Start the local SQLite dev stack with a single command. All five services launch as background processes.

./run.sh start        # start all 5 services
./run.sh status       # verify health + PIDs

Expected status output — all five services healthy:

  SERVER    PORT    PID     HEALTH
  fhir      8080    12345   healthy
  auth      8088    12346   healthy
  emr       3000    12347   healthy
  portal    8090    12348   healthy
  dataset   8091    12349   healthy

Health Check Links

FHIR Health ↗ Auth Health ↗ EMR ↗ Portal ↗ Dataset Worker ↗
Tip: FHIR health returns {"backend":"sqlite"} — this confirms you are on the local SQLite path, not Docker. Docker returns {"backend":"postgres"}.

Presenter Checklist

  • FHIR health at :8080 returns {"backend":"sqlite"}
  • Auth health at :8088 returns {"status":"ok"}
  • EMR frontend loads at :3000 (may take 3–5 s on first load)
  • Dataset Worker health at :8091 returns {"status":"ok"}

Step 2 — EMR Login as Doctor

Local · run.sh
  1. 1

    Open the EMR login page

    Open EMR Login ↗
  2. 2

    Enter Doctor credentials

    doctor@emr.local
    doctor123

    The EMR uses OAuth2 implicit flow — login redirects to Auth Server at :8088, which returns a JWT in the URL hash.

  3. 3

    Navigate to Patients — click Patients in the left sidebar. You will see the seeded demo patients: Alice, Bob, Carol, David.

  4. 4

    Open a patient chart — click any patient name to open their chart. Explore the Encounters, Observations, and Appointments tabs.

  5. 5

    (Optional) Login as Admin to show the full admin dashboard. The admin account sees all patients and can access system configuration.

    admin@emr.local
    admin123
Local SQLite note: The Scheduling tab and PractitionerRole search may show empty in local mode. This is a known SQLite limitation — switch to Docker to see full Postgres-backed results.

Presenter Checklist

  • Logged in as Doctor — EMR home screen visible
  • Patient list shows seeded patients (Alice, Bob, Carol, David)
  • Patient chart opened — Encounters tab visible
  • Observations or appointments visible on the chart

Step 3 — FHIR Admin: Search Resources

Local · run.sh
  1. 1

    Open the FHIR Admin UI — served by the Portal server on :8090

    Open FHIR Admin ↗
  2. 2

    Verify the connection dot is green — top of the page shows the FHIR base URL with a green "●" indicator. If amber, click the "Check" button.

  3. 3

    Search Patients — the Search tab is active by default. Select Patient from the resource type dropdown, set count to 20, and click Search.

  4. 4

    View raw FHIR JSON — click any row to expand and see the raw FHIR R4 JSON for that resource.

  5. 5

    Try other resource types: Encounter, Observation, Practitioner, Organization, PractitionerRole

Architecture note: The FHIR Admin UI is a plain HTML/JS file. It calls HFS directly at http://localhost:8080 — no proxy, no auth token needed in local mode. In Docker, it proxies through the Dataset Worker's auth layer.

Presenter Checklist

  • FHIR Admin UI loaded — connection indicator green
  • Patient search returned seeded patient records
  • Raw FHIR JSON visible in the row expander
  • At least one other resource type searched successfully

Step 4 — Bulk Import a Clinical Bundle

Local · run.sh

The FHIR Admin UI can import any FHIR R4 transaction bundle. Six pre-built clinical datasets are included.

FHIR Admin UI ↗ Portal ↗

Pre-built Demo Datasets

FilePatientScenario
01-breast-cancer.jsonEmma ChenBreast cancer — diagnosis, chemo, follow-up
02-stemi.jsonJames RodriguezSTEMI — emergency, PCI, rehab
03-colorectal-ca.jsonDavid ParkColorectal cancer — staging, surgery, chemo
04-heart-failure.jsonMargaret O'BrienHeart failure — chronic management, readmission
05-healthy-male.jsonMichael TorresAnnual wellness checks — 5-year longitudinal
06-healthy-female.jsonSarah KimAnnual wellness checks — 5-year longitudinal
  1. 1

    Click "Bulk Upload" tab in FHIR Admin UI (second tab from left)

  2. 2

    In the "Demo Files" panel on the right, click one of the dataset buttons (e.g., "Emma Chen - Breast Cancer"). The bundle loads into the upload text area.

  3. 3

    Click "Upload to FHIR" — the panel below shows a per-resource result table. Green rows = success, red rows = error.

  4. 4

    Verify in Search tab — go back to Search, select Patient, and confirm the new patient (Emma Chen) now appears.

Format support: The upload tab accepts FHIR transaction bundles, arrays of resources, or individual resource JSON. Each resource is PUT idempotently — safe to run multiple times without creating duplicates.

Presenter Checklist

  • Bulk Upload tab opened
  • Demo bundle loaded via the Demo Files panel
  • Upload executed — green result rows visible
  • New patient appears in Search tab

Step 5 — Dataset Studio: AI-Driven FHIR Generation

Local · run.sh

The Dataset Studio connects to an OpenAI-compatible LLM to generate synthetic FHIR R4 bundles on demand. A template fallback works even without an LLM endpoint.

FHIR Admin UI ↗ Dataset Worker health ↗
  1. 1

    Click "Dataset Studio" tab in FHIR Admin UI (fourth tab)

  2. 2

    Check the Dataset Worker URL — pre-filled as http://localhost:8091. This is the server-side LLM caller that keeps API keys off the browser.

  3. 3

    Select provider mode:

    • Template only — generates a deterministic FHIR bundle with no LLM call. Always works. Use this if no LLM is configured.
    • OpenAI-compatible — calls any vLLM / Ollama / OpenAI-style server at the configured base URL.
    • Legacy DGX1 — calls the university's DGX1 server at /v1/text.
  4. 4

    Click "Use Template" or "Generate Dataset" — the FHIR JSON textarea fills with a transaction bundle containing Patients, Encounters, Observations, and Practitioners.

  5. 5

    Click "Upload to FHIR" — the Dataset Worker validates the bundle and PUTs each resource to HFS. A success/error row appears per resource.

  6. 6

    Check Dataset Job History — click "Refresh Jobs" to see the persisted job record: type, status, timestamp, user, and entry count.

Architecture: The Dataset Worker (dataset-worker/server.js) is a zero-dependency Node.js HTTP server. It calls the LLM server-side (so API keys never reach the browser), validates that the response is a valid FHIR R4 transaction bundle, uploads each resource to HFS, and records a JSONL audit event in /app/audit/dataset-worker.jsonl.
For the presentation: Use Template only mode for a guaranteed live demo. The template generates a complete patient with Organisation, Practitioner, Encounter, and Observation resources in under 1 second.

Presenter Checklist

  • Dataset Studio tab opened
  • Template loaded — valid FHIR JSON visible in output area
  • Upload executed — green result rows visible
  • Dataset Job History shows the new upload job

Step 6 — MCP: Connect Claude AI to the EMR

Local · run.sh

The MCP Server exposes role-gated EMR tools to Claude via the Model Context Protocol. Users authenticate with a one-time PIN.

PIN Authentication Flow

[ User: Browser ] [ Claude Desktop ] │ │ │ 1. Open /mcp-connect │ MCP server runs: node mcp-server/index.js │ 2. Login with EMR creds │ (stdio transport — Claude manages process) │ 3. See 6-digit PIN │ │ │ 4. Call authenticate() — Claude asks for PIN ▼ │ [ Auth Server :8088 ] │ │ POST /auth/mcp/generate │ 5. authenticate({pin: "123456"}) │ (requires valid EMR JWT) │ │ │ → stores PIN (5 min TTL) │ ▼ │ │ Auth :8088 POST /auth/mcp/exchange │◄───────────────────────────────│ → returns {role, email, fhir_user_id} │ 6. Session established │ │ │ 7. Role-gated FHIR tools now available │ [ HFS :8080 ] │ │ GET /Patient, /Encounter, /Observation …
  1. 1

    Open the MCP Connect page in the EMR:

    Open MCP Connect ↗
  2. 2

    Log in with your EMR credentials — the page generates a 6-digit PIN visible on screen. The PIN expires in 5 minutes.

  3. 3

    Add the MCP server to Claude Desktop — edit ~/.config/claude/claude_desktop_config.json:

    {
      "mcpServers": {
        "openseva-emr": {
          "command": "node",
          "args": ["/path/to/openseva/mcp-server/index.js"]
        }
      }
    }
  4. 4

    In Claude, call authenticate — first call with no arguments to get instructions, then with the PIN:

    authenticate()
    # → Claude shows instructions and MCP connect link
    
    authenticate({"pin": "123456"})
    # → Authenticated as doctor@emr.local (practitioner)
    # → Role-gated tools now available

Role-Gated MCP Tools

RoleAvailable Tools
Admin All tools: patient search, encounters, medications, appointments, system info, user listing
Doctor search_patients, get_patient, list_encounters, get_encounter, list_medications, list_observations, get_patient_summary
Receptionist search_patients, get_patient, list_appointments, check_in_patient, schedule_appointment
Patient my_records, my_appointments, my_medications (own data only)
Example Claude queries after authentication:
  • "List all patients with diabetes"
  • "Show me Emma Chen's last 3 encounters"
  • "What medications is James Rodriguez currently taking?"
  • "Summarise David Park's clinical history"

Presenter Checklist

  • MCP Connect page opened and PIN visible
  • MCP server config shown (Claude Desktop JSON)
  • PIN exchange flow explained to advisor
  • Role-gated tool table shown

Step 7 — Switch to Docker (PostgreSQL)

Docker · Postgres

The Docker stack runs all services in containers with PostgreSQL, stable JWT keys, and enforced FHIR authentication. Both stacks can run simultaneously — they use different port ranges.

Prerequisite: Docker Desktop must be running. The first build compiles the Rust HFS binary from source — allow 10–15 minutes. Subsequent starts use cached images and take ~30 seconds.
docker compose --profile admin up -d   # start all services
docker compose --profile admin ps      # verify all containers healthy

Stack Comparison

AspectLocal (run.sh)Docker (Postgres)
StorageSQLite filePostgreSQL 16 (Docker volume)
FHIR authDisabled (open)Enabled — RS256 JWT required
JWT keysEphemeral (new each restart)Stable (set via env vars)
User passwordsHardcoded in JSbcrypt (cost 12) in PostgreSQL
PractitionerRole searchBroken (SQLite)Fixed (Postgres)
EMR port30003100
FHIR port80808180
Auth port80888188
FHIR Admin port80908181
Dataset Worker80918191
Swagger UI8182
Data persistenceLocal file (dev only)Docker volume (survives restart)

Health Check Links (Docker)

FHIR Health ↗ Auth Health ↗ EMR Login ↗ FHIR Admin ↗ Swagger UI ↗ Dataset Worker ↗

Presenter Checklist

  • All Docker containers show "Up (healthy)" in docker compose ps
  • FHIR health at :8180 returns {"backend":"postgres"}
  • Auth health at :8188 returns {"db":"connected"}
  • EMR login page loads at :3100

Step 8 — JWT Auth Enforced: No Token = 401

Docker · Postgres

In Docker mode, HFS validates every request against an RS256 JWT. Unauthenticated writes are rejected with 401 Unauthorized. This is the key security difference from local dev.

Demonstrate the 401

curl -s -o /dev/null -w "%{http_code}" \
  http://localhost:8180/Patient
# Expected: 401

Authenticated Request (with admin token)

# 1. Login and capture token
TOKEN=$(curl -s -X POST http://localhost:8188/auth/api/login \
  -H "Content-Type: application/json" \
  -d '{"email":"admin@emr.local","password":"admin123"}' \
  | python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])")

# 2. FHIR Patient search with token
curl -s -H "Authorization: Bearer $TOKEN" \
  "http://localhost:8180/Patient?_count=5"
# Expected: 200 + FHIR bundle JSON

Verify the JWKS Endpoint

curl http://localhost:8188/.well-known/jwks.json
# Returns: RSA public key used by HFS to validate all JWTs
View JWKS ↗ Swagger UI ↗
How it works: The Auth Server generates an RSA-2048 key pair on startup (or uses the stable keys from JWT_PRIVATE_KEY_PEM env var). HFS fetches the public key from http://auth:8088/.well-known/jwks.json (Docker internal network) and verifies every incoming JWT's RS256 signature, issuer (http://localhost:8188), and audience (openseva-fhir).
Local vs Docker: In run.sh mode, HFS_AUTH_ENABLED is explicitly set to empty — FHIR accepts all requests. This is intentional for lightweight local development. The Docker Compose file sets HFS_AUTH_ENABLED=true and wires the JWKS URL.

Presenter Checklist

  • Curl without token to :8180 returns 401
  • JWKS endpoint at :8188 returns RSA public key JSON
  • Curl with admin token returns 200 + FHIR bundle
  • JWKS / RS256 JWT flow explained to advisor

Step 9 — Docker FHIR Admin + Persisted Job History

Docker · Postgres
  1. 1

    Open Docker FHIR Admin:

    Open Docker FHIR Admin ↗
  2. 2

    Login via the Auth panel — at the top of the page, enter admin credentials and click "Login". The bearer token is stored in session storage and forwarded to all FHIR and Dataset Worker requests.

    admin@emr.local
    admin123
  3. 3

    Search Patients — the Search tab uses PostgreSQL via HFS. Note that results are faster and the total field is present (not available in SQLite).

  4. 4

    Dataset Studio → Refresh Jobs — click "Refresh Jobs" to load persisted job history from the Docker volume at $DATA_DIR/audit/dataset-jobs.json (default ./docker-data). Jobs survive container restarts.

  5. 5

    Open Swagger UI — view the auto-generated OpenAPI spec from HFS's CapabilityStatement:

    Open Swagger UI ↗
Persistence: The Docker Dataset Worker stores audit logs at ${DATA_DIR}/audit/dataset-worker.jsonl and job history at ${DATA_DIR}/audit/dataset-jobs.json. Both are bind-mounted from the host — they survive docker compose down and docker compose up.

Presenter Checklist

  • Docker FHIR Admin loaded — admin logged in via Auth panel
  • Patient search returns results (Postgres-backed, faster)
  • Dataset Job History shows persisted jobs from earlier uploads
  • Swagger UI loaded and showing FHIR API endpoints

Architecture: RS256 / JWKS Authentication Flow

Deep Dive

Step-by-step JWT flow

1 · User opens EMR login
GET /auth/authorize → Auth Server returns login HTML page
2 · Submit credentials
POST /auth/authorize (email + password)
Auth Server: bcrypt.verify() against PostgreSQL users table
3 · JWT issued
RS256 token signed with RSA-2048 private key
iss: http://localhost:8188
aud: openseva-fhir
role: practitioner
fhirResourceId: Practitioner/pract-doctor
4 · Token delivered to EMR
Redirect to EMR with token in URL hash fragment
http://localhost:3100/#access_token=eyJ…
5 · EMR uses token for FHIR calls
Authorization: Bearer <JWT>
All FHIR reads/writes include this header
6 · HFS validates JWT
Fetches RSA public key from JWKS endpoint
Verifies RS256 signature + issuer + audience
200 OK or 401 Unauthorized

JWT Payload

{
  "sub":  "fa2d6178-...",
  "email":"doctor@emr.local",
  "role": "practitioner",
  "scope":"system/*.cruds",
  "aud":  "openseva-fhir",
  "iss":  "http://localhost:8188",
  "iat":  1780077594,
  "exp":  1780106394
}

JWKS Response

{
  "keys": [{
    "kty": "RSA",
    "use": "sig",
    "alg": "RS256",
    "kid": "openseva-signing-key-1",
    "n":   "rGNgsb213taJ5bD9tU27...",
    "e":   "AQAB"
  }]
}
MCP PIN flow is separate: POST /auth/mcp/generate (requires valid EMR JWT) → returns 6-digit PIN stored in memory → POST /auth/mcp/exchange by MCP server → user session with role + FHIR user ID.

Role Capability Map

All Stacks
Capability Admin Doctor Receptionist Patient
EMR login
Patient list (all)❌ own only
Patient chart✅ read❌ own only
Encounters❌ own only
Prescriptions✅ write❌ own only
Book appointment✅ any✅ own patients✅ any (front desk)✅ self only
FHIR Admin UI
Dataset Studio
User management API
MCP toolsAllClinicalSchedulingSelf-service

Known Limitations

Honest Assessment
FeatureStatusNotes
PractitionerRole search Fixed in Docker SQLite does not support the JOIN queries HFS uses for PractitionerRole. Postgres resolves this.
SDC $populate / $extract Partial These are Aidbox-specific FHIR operations. HFS does not implement them — inherited EMR screens may show errors.
Video calls (Jitsi integration) Stubbed The inherited EMR code includes a Jitsi button. It returns a placeholder — no real video implementation.
HTTPS / TLS Disabled All services run HTTP locally. Production deployment should add a reverse proxy (nginx/Traefik) with TLS.
AidboxQuery / $query Not available Aidbox proprietary FHIR extension. HFS is not Aidbox. These inherited API calls return errors.
Stable JWT keys (local) Ephemeral Without JWT_PRIVATE_KEY_PEM env var, keys are regenerated on each ./run.sh restart auth. Existing tokens become invalid.
Admin FHIR identity Known gap Admin user maps to Organization/org-local instead of a Person resource. Clinical screens designed for practitioners may behave oddly.
_tag search on HFS Not indexed HFS ignores ?_tag=… and returns all resources. The HIE simulation scopes tenants with ?organization=Organization/org-X instead; meta.tag is a provenance label only.
HIE patient matching (MPI) Deterministic built A working viewer (portal/hie-exchange.html) does deterministic matching (family + DOB + gender) and merges the cross-org record. Fuzzy/probabilistic scoring, consent enforcement, and dedup remain student capstone tickets.
Playwright regression tests Pending No automated browser tests yet. Core workflows have been manually smoke-tested against both stacks.
Demo safety: For the advisor presentation, use the Doctor account for EMR workflows and Admin for FHIR Admin / Dataset Studio. Avoid the Admin account for clinical screens to avoid the Org identity gap.

What's Built So Far

Project Status

A snapshot of OpenSeva's current capabilities. Everything marked Working runs today on both stacks and is demoable. The newest additions turn OpenSeva into a hand-over-to-students teaching platform with a multi-EMR exchange.

CapabilityStatusNotes
Core EMR — patients, encounters, prescriptions, schedulingWorkingReact/Vite clinical app against the HFS FHIR R4 backend.
Four roles + RS256/JWKS authWorkingAdmin · Doctor · Receptionist · Patient. JWT enforced on the Docker stack.
Two stacks — local (SQLite) & Docker (Postgres)WorkingSeparate, configurable port ranges; run simultaneously without conflict.
FHIR data import — bulk bundles + Dataset StudioWorkingFHIR Admin UI; AI-generated synthetic bundles via the Dataset Worker.
Local-LLM AI bridge — summaries, magic search, form builderWorkingDataset Worker /ai/* → OpenAI-compatible local LLM. Self-contained, no external API.
Native appointment bookingWorkingCreates Appointment directly (no SDC $extract, which HFS 405s).
MCP / Claude AI integrationWorkingPIN-based session; role-gated tools over FHIR + Auth.
Student build roadmap + contributor guideNewROADMAP.md (30+ tickets, 6 tracks) & CONTRIBUTING.md. Pick → branch → PR → merge.
HIE multi-EMR simulation — scaffold (E1)New4 tenant EMRs + cross-org patient-overlap scenario. seed-hie.sh, hie/README.md.
HIE Exchange viewer — cross-org match + merged recordNewDeterministic MPI + unified-record UI: portal/hie-exchange.html. Navigable from the Portal; drills into Patient 360.
Revenue Cycle Simulation — coding → draft claim → billing → simulated submitNewProvider-side claims lifecycle for teaching. Explainable code suggestions, audit trail, X12 837 preview, burden dashboard. See section.
Payer App + Payment Integrity — standalone services, real FHIR Claim/ClaimResponse + Coverage, downcoding/fraud/payable-amount, EMR live statusNewSeparate payer app (8092) + payment-integrity API (8093). EMR submits FHIR Claims; payer adjudicates and writes ClaimResponse; 16 sample insurers. See section.
Fuzzy/probabilistic MPI · consent · dedup (E2+)PlannedStudent capstone tickets — the data + a deterministic baseline viewer are in place to build on.

Revenue Cycle Simulation

Educational

Most EMR teaching stops at the visit. The Revenue Cycle Simulation lets students experience what happens after a patient appointment — where the bulk of US healthcare administrative cost lives. It is a process teaching tool, not a billing engine: there is no payer adjudication, clearinghouse, real X12/EDI, payment, or denial logic. All codes are clearly labelled educational sample codes, never claimed to be billing-valid, and a human must approve every step. Language stays neutral ("coding review", "claim readiness", "documentation support") — the simulation deliberately does not teach upcoding.

The workflow

Appointment → Check-in → Encounter → Signed Note → Coding Review → Draft Claim → Billing Review → Simulated Submission

  1. Sign & Start Coding Review — on a finished encounter the provider signs the note (records a FHIR Provenance attestation) and a coding-review task is created.
  2. Coding Review — the helper suggests diagnosis/procedure codes. Each suggestion is explainable: it shows the exact note text that supports it, plus a confidence score and reason. The reviewer accepts, rejects, edits, or adds a code, and can add a documentation note.
  3. Draft Claim — after documentation validation, accepted codes are assembled into a draft claim.
  4. Billing Review (Admin acts as Billing Specialist) — readiness checklist, status timeline, and a conceptual X12 837-style preview. Send back to coding, mark ready, or Simulate Submit.
  5. Submit to Payer — status becomes Submitted and a real FHIR Claim is sent to the standalone Payer App, which adjudicates it; the EMR Claim Status view then shows the live result.

Coding helper — two modes

  • AI-assisted — when the local LLM is configured, the note is sent to the Dataset Worker /ai/coding-suggest bridge with strict guardrails (education only, never optimize reimbursement, never invent documentation, only note-supported codes, warnings when documentation is thin). Runs fully on-device on Apple Silicon via the MLX/ module (./MLX/start-llm.sh) — no external API.
  • Rule-based fallback — a transparent keyword matcher (diabetes, hypertension, annual wellness, chest pain, follow-up …) that extracts real evidence sentences from the note. Used automatically when the LLM is unavailable, so the workflow always works offline.

Roles

RoleCan do
Doctor / ProviderSign notes, run coding review, accept/edit codes, generate draft claims. Does not submit final claims.
Admin (= Billing Specialist)All queues, billing review, mark ready, simulate submit, configure. A dedicated billing role is a one-line add later.
ReceptionistView claim status only — cannot edit clinical codes.
PatientNo access — coding/claim details are not shown to patients in this phase.

Under the hood

  • Data: each claim is a SimulatedClaim persisted as a FHIR Basic resource (stable id simclaim-<encounterId>, full payload in an extension) — shared across roles, persistent, and designed to map to a real FHIR Claim later.
  • Audit trail: every action (signed, suggested, accepted/rejected/edited, draft generated, billing review, simulated submit) is recorded on the claim and surfaced in the billing timeline.
  • Burden dashboard: an "Educational administrative burden simulation" card shows queue counts, average human touches per claim, and warnings generated — a teaching aid, not financial truth.

Try it

  1. Seed the demo scenarios: ./run.sh seed (loads 5 claims incl. an incomplete documentation case).
  2. Login as doctorRevenue Cycle → Coding Review Queue; open a claim and accept codes; Generate Draft Claim.
  3. Login as admin → Billing Review; review the checklist + X12 preview; Mark Ready, then Simulate Submit.
  4. Open Learning View for the step-by-step diagram of who does what, which FHIR resource is involved, and why each step adds administrative cost.

Payer App + Payment Integrity (standalone services)

Educational · Phase 3

Revenue Cycle stops when the provider clicks Submit. From there the claim leaves the EMR for a separate insurance-company app running on its own port — exactly like real life, where the payer is a different organisation with its own systems. It is educational only: there is no real payer, clearinghouse, X12 837/835/EDI, CPT/ICD validation, fee schedule, eligibility, or prior authorization. Every amount is a labelled sample. The framing is neutral: the provider wants accurate, documentation-backed payment; the payer wants policy- and documentation-backed payment; the friction between them is the administrative burden.

Two standalone services + the EMR

ServicePortRole
EMR (admin)3000Builds a FHIR Claim from the coded visit + the patient's Coverage, submits it to the Payer App, and shows live status.
Payer App payer-app/8092Receives the FHIR Claim, runs the pipeline, writes Claim + ClaimResponse to FHIR, and serves its own dashboard.
Payment Integrity payment-integrity/8093A separate API the Payer App calls for downcoding, fraud scoring, and the payable amount.

The pipeline (shown in real time; batch in reality)

received → validate → eligibility / coverage → payment integrity (downcoding + fraud + payable amount) → adjudicate → write FHIR Claim + ClaimResponse

  1. Admin submits from Billing Review → Submit Claim to Payer. A modal shows the patient's insurance on file (from Coverage) and a payer dropdown; a real FHIR Claim is POSTed to the Payer App.
  2. Validate — missing patient / provider NPI placeholder / diagnosis / procedure / coverage → rejected.
  3. Eligibility / coverage — unknown plan, or a service the plan excludes → denied.
  4. Payment integrity — the Payer App calls the integrity API: it detects downcoding (high-complexity code vs a per-plan documentation bar), scores fraud/abuse (upcoding pattern, excessive units, dx/proc mismatch, thin doc), and computes the payable amount from the plan's coverage %, copay, and deductible.
  5. Adjudicate & persist — outcome is paid / downcoded / denied / rejected; the Payer App writes the ClaimResponse to FHIR. The EMR's Claim Status view polls FHIR and updates live.

Outcomes & sample payment math (simulation only)

OutcomeWhen
paidClean claim — payment = allowed × coverage% − copay − deductible.
downcodedHigh-complexity code not supported by documentation; payer lowers the payable level and pays less.
deniedService not covered by the plan, or blocked by the fraud screen.
rejectedRequired claim data missing/invalid — fails before payment review.

Numbers are obvious placeholders — no real fee schedule. Each of the 16 sample insurers (payer-app/data/payers.json) has its own rules, so the same visit pays differently per plan. Example (Aetna): allowed 180 − $50 deductible × 80% − $30 copay = $74; BlueShield: allowed 180 × 85% − $25 = $128.

FHIR resources (the shared DB is the system of record)

  • Coverage + insurance Organizations model patient insurance (seed-insurance.sh links 13 patients to plans).
  • Claim (built by the EMR, id claim-<encounterId>) carries the note + chosen payer in http://openseva.local/claims/* extensions.
  • ClaimResponse (written by the Payer App, id claimresponse-<encounterId>) holds the outcome, adjudication amounts, payment, process notes, and fraud/downcoding extensions. The EMR reads this back for real-time status.

Roles

RoleSubmit claim to payerView claim status (EMR)Payer App dashboard (8092)
Admin✓ (separate app)
Practitioner✓ (separate app)
Receptionistview only✓ (separate app)
Patient— (Revenue Cycle blocked)

The Payer App dashboard is a separate service with no EMR login — it is the insurance company's view, deliberately outside the EMR's role model.

Try it

  1. Start everything (incl. the two payer services) and seed insurance: ./run.sh start then ./run.sh seed.
  2. Login as admin → Revenue Cycle → Billing Review on a ready claim → Submit Claim to Payer (the patient's plan is prefilled from Coverage).
  3. Watch Claim Status update live as the payer adjudicates, then open the Payer App dashboard to see the same claim from the insurer's side.
  4. Try a downcode (high-complexity code on a short note) and a rejection (a claim missing a diagnosis) to see the other outcomes.

HIE Simulation — Multiple EMRs, One Patient Record

New · Track E

An EMR is one clinic's chart. An EHR is a record designed to follow the patient. A Health Information Exchange (HIE) is the infrastructure that moves records between organisations and assembles one longitudinal record. OpenSeva now simulates an HIE: four EMR "tenants" feed a shared FHIR store, and the genuinely hard interoperability problems — patient matching, consent, deduplication, provenance — become concrete, gradeable student projects.

Open HIE Exchange Viewer ↗ Patient 360 — Priya ↗ Portal ↗

Topology (tag-based, single store)

[ Clinic A ]──┐ [ Hospital B ]─┤ [ Lab C ]──────┼──►[ HFS FHIR :8080 ] [ Pharmacy D ]─┘ │ every resource │ meta.tag = source ▼ [ Exchange View ] │ match identities (MPI) │ merge cross-org record ▼ Unified longitudinal record

The four tenants

TenantOrganizationHolds
Clinic Aorg-clinic-aConditions, encounters
Hospital Borg-hospital-bInpatient admissions, conditions
Lab Corg-lab-cLab orders + results
Pharmacy Dorg-pharmacy-dPrescriptions + dispenses

Scope a tenant with ?organization=Organization/org-X. (HFS does not index _tag — it's a provenance label, not a filter.)

The cross-org matching scenario (the teaching payoff)

The seed deliberately registers the same human at several orgs with slightly different demographics and a different MRN each — exactly what a Master Patient Index must resolve. No single EMR sees the whole picture.

Real personRegistered atThe twistWhat only the merged record shows
Priya Sharma Clinic A · Hospital B · Pharmacy D Middle name & phone differ across orgs Diabetes (A) + Hypertension (B) + active Metformin (D)
John Doe Clinic A · Lab C Fuzzy: Lab C has given name "J", not "John" Abnormal glucose lab (C) alongside his clinic chart (A)
Grace Lee / Omar Farah One org each Single-org controls Must not match anyone — guards against false positives

Load the HIE demo

./run.sh start          # stack up (or: docker compose up -d)
./run.sh seed           # base single-clinic demo (optional)
./seed-hie.sh           # the multi-EMR HIE layer (idempotent)

Verify a tenant-scoped view and confirm Priya's record is split across orgs:

# Hospital B's patients only (→ Priya + Omar)
curl 'http://localhost:8080/Patient?organization=Organization/org-hospital-b'

# Priya's diabetes lives only at Clinic A; her Metformin only at Pharmacy D
curl 'http://localhost:8080/Condition?subject=Patient/hie-a-priya'
curl 'http://localhost:8080/MedicationDispense?subject=Patient/hie-d-priya'
Demo line for your advisor: "Each EMR only knows its slice — Clinic A sees diabetes, the hospital sees hypertension, the pharmacy sees the prescription. The HIE's job is to recognise these are one person and assemble the complete record. That recognition problem — the Master Patient Index — is the student capstone." Full model in hie/README.md.

Student Roadmap & Contributing

Hand-over

OpenSeva is built to be handed to students: they pick a ticket from ROADMAP.md, build it, and raise a PR; maintainers review and merge. Every ticket lists the FHIR resources, acceptance criteria, and exact files to touch — no reverse-engineering required. Setup and the PR workflow live in CONTRIBUTING.md.

TrackThemeExample tickets
AQuick wins / known-gap fixes StarterAdmin User Management UI · admin→Person mapping · patient provenance
BClinical chart IntermediateConditions · Allergies · Immunizations · Vitals trends · Labs · SOAP notes
CWorkflow & operations IntermediateLab orders · e-Prescribing · native scheduling · AuditEvent · billing
DInteroperability & standards AdvancedBulk $export · FHIR Subscriptions · SMART-on-FHIR launch
EHIE simulation CapstonePatient matching (MPI) · unified record · cross-org consent · dedup
FAI & data generation MixedAI record summary · suggest ICD-10/SNOMED · synthetic cohorts · CDS alerts

Hand-over workflow

  1. 1
    Student picks a ticket and opens a GitHub issue to claim it.
  2. 2
    Branch: feat/B1-conditions.
  3. 3
    Build against the acceptance criteria; test across roles.
  4. 4
    Raise a PR with screenshots + how-to-test.
  5. 5
    Maintainer reviews & merges.

Good first tickets

All 🟢 Starter — well-scoped, few files, teach the codebase patterns:

  • A1 — Admin User Management UI (the auth API already exists)
  • A3 — Patient provenance ("recorded by Dr. X")
  • B1 / B2 / B3 — Conditions · Allergies · Immunizations tabs
Reference docs: ROADMAP.md, CONTRIBUTING.md, and hie/README.md live at the repo root.