A FHIR-native Electronic Medical Records UI built with Next.js 16 and React 19. It connects directly to a FHIR R4B server with no proprietary backend — all clinical operations go through standard FHIR REST APIs.
Pyronis is also a practical learning tool for anyone who wants to understand how FHIR works in a real application. Every resource the UI touches maps directly to a standard FHIR resource type. The built-in Raw JSON viewer (available on every detail page) lets you inspect the exact FHIR representation of what you are looking at, and the Subscription + Notification module gives you a hands-on way to see how FHIR event-driven workflows behave end to end.
Status: Active development — core clinical workflows are implemented and usable. See the feature gap analysis for what is planned next.
![]() |
![]() |
| JWT login — or continue without a token against an open server | Dashboard — stat cards, recent registrations and encounters |
![]() |
![]() |
| Patient header with clinical signal chips and tabbed clinical data | Vitals tab — latest readings for all 8 vital types |
![]() |
![]() |
| Chart summary — active problems, medications, allergies and encounter history | Patient merge — side-by-side field comparison, amber rows highlight differences |
Encounter detail — patient context bar, SOAP note (S/O/A/P), and tabbed clinical sections (Vitals, Clinical, Documents, Discharge Rx)
![]() |
![]() |
| Critical vital sign alert — critical values in red, abnormal in amber | Add patient flag — category, status and effective date |
![]() |
![]() |
| Discharge prescription builder — dose, route, frequency, duration with live sig preview | GAD-7 questionnaire — button-group answers, auto-scored on submit |
![]() |
![]() |
| MAR — order active, first dose refused by patient | MAR — second dose recorded as given, full administration history |
![]() |
![]() |
| Global worklist — filter by status and priority across all patients | Organisation detail with linked practitioners |
![]() |
![]() |
| Practitioner detail — qualifications and organisation roles | Raw FHIR JSON viewer — available on every resource detail page |
Patient face sheet — print-ready layout with demographics, problem list, allergies, medications and active alerts
- Full demographic CRUD — English + Arabic bilingual names, photo (webcam or upload), MRN, QID, passport
- Rich demographics — nationality, ethnicity, person type, birth place, insurance, VIP flag, cadaveric donor, deceased status
- EMPI / QID deduplication on registration
- Patient chart summary — active problems, medications, allergies, vitals, encounter history
- Print-ready face sheet
- Appointments — book, reschedule, cancel, check-in, fulfil; month/week/day calendar view
- Encounters — start, close, SOAP notes (rich text, printable), discharge summary
- Vitals / Observations — batch entry, LOINC-coded, per-encounter and per-patient views
- Problem List — active / resolved conditions, ICD-10, promote encounter-diagnosis to problem list
- Medications — discharge Rx with dosage builder (printable); inpatient MAR with administration recording
- Orders — lab, radiology, procedure; priority, indication, cancel
- Procedures — order and record performed procedures
- Immunizations — CVX codes, lot, site, route, dose number, series
- Allergies & Intolerances — substance, criticality, reaction severity
- Flags / Alerts — categories, colour-coded, displayed on patient header
- Diagnostic Reports — LAB/RAD/PATH/REF/GEN with file attachments
- Referrals — create, edit, status tracking, per-encounter card, global list
- Family History — relationships, conditions, deceased status
- Related Persons — next of kin, emergency contacts, guarantors
- Advance Directives — DNR/DNI/POLST displayed on patient header
- Questionnaires — PHQ-9, GAD-7, AUDIT-C, Patient Intake; auto-scoring with severity labels
- Tasks / Worklist — 6 categories, priority, due date, assignee; global and per-patient views
- Document Management — file upload, download, delete; 9 document types
- Practitioner directory — CRUD, specialties, qualifications, role-to-organisation links
- Organisation registry — CRUD, hierarchy (
partOf), linked practitioners - Locations — CRUD for physical sites; status, mode, type, address, managing organisation, part-of hierarchy
- Healthcare Services — CRUD; specialty, availability hours and exceptions, linked location and organisation
- Devices — CRUD; 11 device types, UDI, serial number, manufacturer, model, asset code, linked organisation and location
- Settings — FHIR server URL, eMPI URL configurable at runtime
- Subscriptions — create and manage FHIR R4 subscriptions using the R4B Subscriptions Backport IG. Supports topic URL, filter criteria, channel type (rest-hook, websocket, email, SMS, message), payload content level, heartbeat period, and custom HTTP headers.
- Webhook endpoint —
POST /api/fhir/notifyreceives incoming notification bundles from the FHIR server, tags them, and re-stores them asBundleresources directly on the FHIR server — no separate database needed. - Live notification panel — bell icon in the header fetches the latest notification bundles on load. Each notification links directly to the triggering resource. Dismiss removes the bundle from the FHIR server.
- Notification inbox (
/notifications) — full table of all received notifications with resource type badge, resource link, subscription reference, received timestamp, raw JSON viewer, and individual dismiss.
- Dark-navy sidebar with collapsible icon-only mode (state persisted)
- Patient tab clustering — 16+ tabs grouped into Clinical / Administrative / Documents & Reports
- Clinical signal chips on patient header — active encounter, problems, medications, tasks, critical allergies
- Skeleton loading screens and failed-fetch banners for every route
- Raw FHIR JSON viewer on every resource detail page
- Unified
StatusPillcomponent across all resource types - JWT token login with middleware-protected routes
Pyronis is built on vanilla FHIR — every button in the UI maps to a documented FHIR operation. This makes it a useful sandbox for exploring the spec hands-on.
| What you do in the UI | What happens on the FHIR server |
|---|---|
| Register a patient | POST /Patient with MRN identifier, extensions, bilingual names |
| Open a patient record | GET /Patient/{id} |
| Start an encounter | POST /Encounter with subject reference to the patient |
| Record a vital sign | POST /Observation with LOINC code, valueQuantity, linked encounter |
| Add a diagnosis | POST /Condition with clinicalStatus, code (ICD-10), encounter context |
| Prescribe a medication | POST /MedicationRequest with structured dosage |
| Record administration | POST /MedicationAdministration referencing the order |
| Upload a document | POST /DocumentReference with content[0].attachment.data (base64) |
| Create a location | POST /Location with address, type, status, managing organisation |
| Create a device | POST /Device with UDI carrier, device name, manufacturer, linked location |
| Create a subscription | POST /Subscription with backport IG extensions for topic and filter |
| Receive a notification | FHIR server POSTs a Bundle to /api/fhir/notify; re-stored with a tag |
Every detail page has a "{ } JSON" button that opens the raw FHIR resource exactly as it is stored on the server. This is the fastest way to see how a UI concept translates into FHIR JSON without calling the API directly. Try it on a Patient after adding an Arabic name, or on a Subscription to see the backport IG extensions in place.
The Subscriptions module gives a hands-on view of FHIR event-driven workflows:
- Create a subscription at
/subscriptions/new. Set the topic to a SubscriptionTopic canonical URL your server supports. Set the endpoint to<your-app-url>/api/fhir/notify— the "Use this app's webhook" button fills this in automatically. - Trigger an event — create or update the resource type the subscription watches.
- Check the inbox at
/notifications. The notification bundle the FHIR server delivered will appear there, showing the resource type, ID, and a direct link to the resource's detail page. - Inspect the raw bundle using the JSON viewer to see the
SubscriptionStatusentry,notificationEvent[].focusreference, and the taggedmeta.
- fhir-candle — lightweight .NET FHIR server with a built-in web UI for browsing stored resources. Tested and recommended for local development.
- FHIR R4B specification — the reference for every resource type Pyronis uses.
- Subscriptions Backport IG — describes the extension pattern Pyronis uses for topic-based subscriptions on R4/R4B servers.
| Concern | Package | Version |
|---|---|---|
| Framework | next |
16.2.7 |
| React | react |
19.2.4 |
| UI components | shadcn (v4) |
^4.10.0 |
| UI primitives | @base-ui/react |
^1.5.0 |
| FHIR types | @medplum/fhirtypes |
^4.5.2 |
| FHIR utilities | @medplum/core |
^4.5.2 |
| Icons | lucide-react |
^1.17.0 |
| CSS | Tailwind v4 + tw-animate-css |
^4 |
- Node.js 20+ and npm
- A running FHIR server — fhir-candle is the tested and recommended option for local development.
Start a local fhir-candle as a .NET tool:
fhir-candle -oIf you don't have the tool installed:
dotnet tool install --global fhir-candleThe FHIR R4B base URL will be http://localhost:5826/fhir/r4b.
fhir-candle also exposes R4 at /r4 and R5 at /r5 on the same port. The web UI (useful for inspecting stored resources) is available at http://localhost:5826.
# 1. Clone the repo
git clone https://github.com/berkant-k/pyronis.git
cd pyronis
# 2. Install dependencies
npm install
# 3. Configure environment
cp .env.example .env.local
# Edit .env.local — at minimum set NEXT_PUBLIC_FHIR_BASE_URL
# 4. Start the development server
npm run devOpen http://localhost:3000.
Create a .env.local file at the project root:
NEXT_PUBLIC_FHIR_BASE_URL=http://localhost:5826/fhir/r4b
NEXT_PUBLIC_EMPI_BASE_URL=http://localhost:5826/fhir/r4b| Variable | Description |
|---|---|
NEXT_PUBLIC_FHIR_BASE_URL |
FHIR R4B server base URL |
NEXT_PUBLIC_EMPI_BASE_URL |
eMPI server base URL (can also be overridden at runtime from the Settings page) |
Both variables are public (browser-read) because Pyronis talks to FHIR directly with no intermediary backend.
npm run dev # development server with hot reload
npm run build # production build
npm run start # serve production build
npm run lint # ESLint
npx tsc --noEmit # TypeScript type check (no output = clean)src/
app/ Next.js App Router pages
patients/ Patient list, detail, new, edit
encounters/ Encounter detail with clinical tab sections
appointments/ Appointment list + calendar
practitioners/ Practitioner directory
organizations/ Organisation registry
locations/ Location CRUD
healthcare-services/ Healthcare Service CRUD
devices/ Device CRUD
subscriptions/ Subscription CRUD
notifications/ Notification inbox
questionnaires/ Questionnaire library
tasks/ Global task worklist
reports/ orders/ medications/ flags/ ...
api/fhir/notify/ POST endpoint — receives and stores FHIR notification bundles
components/
patients/ PatientForm, PatientSearch
notifications/ DismissNotificationButton
layout/ Sidebar, Header (with live NotificationPanel)
ui/ Shared primitives (PatientBanner, StatusPill, RawFhirDialog, …)
reports/ orders/ ... Feature-specific cards and dialogs
lib/
fhir-client.ts All FHIR fetch/mutate operations and display helpers
empi-client.ts eMPI query helpers
auth.ts JWT token storage (localStorage + cookie)
questionnaires.ts Built-in questionnaire definitions
config.json Deployment-configurable values (identifier systems, extension URIs, code lists)
- No backend — the UI calls the FHIR server directly via
fetch. All operations go throughsrc/lib/fhir-client.ts. - Server components fetch data at request time (
page.tsx). Client components ("use client") handle state, forms, and event handlers. - Patient photos are stored as
Patient.photo[0]base64 data URIs on the FHIR resource. - Bilingual names — Arabic name is a second
Patient.nameentry carrying the HL7 language extension (valueCode: "ar"). - MRN generation uses
nanoidcustomAlphabet("0123456789", 10)— CSPRNG, no server round-trip, collision-safe. - All deployment-configurable values (identifier systems, extension URIs, code lists) live in
src/lib/config.json.
FHIR server Pyronis
──────────────────────────────── ────────────────────────────────────────
Subscription resource created ← POST /Subscription (via subscriptions UI)
Resource event fires → FHIR server POSTs to /api/fhir/notify
Route tags bundle, re-POSTs to FHIR server
as Bundle with tag=notification
Notification inbox loads ← GET /Bundle?_tag=notification&_sort=-_lastUpdated
Resource link clicked ← Parsed from SubscriptionStatus.notificationEvent[].focus
Notification bundles are stored directly on the FHIR server — no additional database is needed.
@medplum/fhirtypes ships R4 Subscription types which do not include the R4B topic or filterBy fields. Pyronis uses the Subscriptions Backport IG extension pattern to attach these on the R4 shadow elements (_criteria, channel._payload, channel.extension). Three accessor helpers in fhir-client.ts parse the extensions back out:
subscriptionFilterCriteria(sub) // → backport-filter-criteria extension value
subscriptionHeartbeat(sub) // → backport-heartbeat-period extension value
subscriptionPayloadContent(sub) // → backport-payload-content extension valuePyronis was originally designed for the Gulf healthcare environment and aligns with Qatar Ministry of Public Health (MoPH) FHIR profiles. These customisations are active by default but all identifiers and extension URIs are declared in src/lib/config.json and can be overridden for other regions.
| Identifier | System | Notes |
|---|---|---|
| MRN | https://pyronis.health/mrn |
10-digit numeric, CSPRNG-generated |
| QID | http://hl7.org/fhir/sid/nn |
Qatar National ID — 11-digit national number |
| Passport | http://hl7.org/fhir/sid/ppn |
HL7 passport number |
QID lookup against an external eMPI server is built into the patient registration flow to detect existing records before a new patient is created.
Extensions used on Patient resources use Pyronis StructureDefinition URIs (https://fhir.pyronis.health/StructureDefinition/…):
The config file should be updated according to MoPH's documents.
| Field | Extension |
|---|---|
| Nationality | patient-nationality |
| Person type | PersonType |
| Ethnicity | Ethnicity |
| Birth place country | patient-birthPlace |
| Cadaveric donor | patient-cadavericDonor |
| Bilingual name language | NameLanguage |
| Address zone / street / building / unit | AddressZone, AddressStreetNumber, AddressBuildingNumber, AddressUnit |
Country codes use the Pyronis Nationality CodeSystem (https://fhir.pyronis.health/CodeSystem/Nationality) — ISO 3166-1 numeric codes (e.g., 634 = Qatar, 682 = Saudi Arabia).
Patient residency status is captured using Qatar-aligned codes:
| Code | Display |
|---|---|
QAT |
Qatari Citizen |
GCC |
GCC National |
RES |
Resident |
VIS |
Visitor |
DIP |
Diplomat |
STD |
Student |
- Each patient record stores two
Patient.nameentries: English (default) and Arabic. - The Arabic name entry carries the
NameLanguageextension (valueCode: "ar") and renders right-to-left. - All name input fields in the patient form have a dedicated RTL Arabic section.
- The patient header, banner, and print pages display both names when available.
Country dropdowns show Qatar first, then the five other GCC states, then the major expat nationalities present in Qatar (India, Pakistan, Nepal, Bangladesh, Philippines, Egypt, Jordan, Lebanon, …), then the rest of the world alphabetically.
To deploy outside the GCC, update src/lib/config.json:
- Replace
fhir.pyronis.healthextension and code system URIs with your jurisdiction's equivalents (or keep them as-is — they are Pyronis-owned URIs with no external dependency). - Swap out the
personTypeoptions list for locally meaningful residency/patient-class categories. - Replace the
countryCodecode system with ISO 3166-1 alpha-2 if preferred. - The country list order in
src/lib/countries.tscan be reordered to put your local countries first.
Contributions are welcome. Please open an issue before starting a large feature so the approach can be discussed.
- Fork the repository and create a branch (
feature/my-featureorfix/my-fix). - Run
npx tsc --noEmitandnpm run lintbefore submitting — PRs will not be merged with type errors or ESLint warnings. - Keep commits focused. Reference the relevant section of MISSING_FEATURES.md if you are implementing a gap item.
- Open a pull request against
main.
MIT — see LICENSE for details.

















