Patients — Registration and Demographics
Audiences: doctor, clinical-buyer, developer
A patient in Dudoxx HMS is a dual-store entity: a
Userrow in Prisma (ddx_api_main) for operational identity (auth, MRN, insurance), and a FHIRPatientresource in HAPI for clinical identity (demographics, consent, family relationships) — linked by UUID with afhirPatientIdfield on the PrismaUserrow.
Business Purpose
Patient registration is the entry point for every clinical workflow. A patient who is not yet registered cannot have appointments booked, visits opened, or clinical data recorded. The registration process must:
- Create a stable identity that persists across visits and practitioners.
- Assign a Medical Record Number (MRN) that serves as the legal identifier for all clinical documents.
- Sync demographics to FHIR so the patient can be found by any FHIR-aware component (lab system, insurance gateway, AI agent).
- Optionally capture family/emergency contacts and insurance coverage at registration time.
Dudoxx HMS satisfies all four requirements through a transactional PatientCreationService that orchestrates Prisma and HAPI writes atomically, with compensation logic if either store fails.
Audiences
- Investor: Patient count is the primary adoption metric. The dual Prisma+FHIR registration enables both operational scalability (Prisma fast-path for appointments) and regulatory compliance (FHIR for insurance, referral, and ePA integration) without maintaining a synchronization job.
- Clinical buyer (doctor/nurse/receptionist): Receptionists register patients through the patient portal form. The system auto-assigns an MRN on creation. Existing patients can be found by name fuzzy search, phone, or MRN. Doctors see full demographics + active conditions + medications in the medical card on the patient detail page.
- Developer/partner:
GET|POST|PUT|PATCH|DELETE /api/v1/patients. Patient UUID (Prismauser.id) is the stable API identifier. FHIR Patient numeric ID is an internal implementation detail resolved byPatientIdResolverService. Fuzzy patient search available viaGET /api/v1/patients?search={term}. Batch info lookup atPOST /api/v1/patients/batch-info. - Internal (ops/support): Patient creation is a 9-step transactional process with state tracking in Prisma. If creation fails mid-flight, the
compensateFhirCreate()path deletes any partially-created FHIR resources. MRN format is controlled byMrnServiceand configured per organization.
Architecture
PatientsCrudController (/api/v1/patients) → PatientsService (orchestrator —
delegates to 11 sub-services):
| # | Sub-service | Responsibility |
|---|---|---|
| 1 | PatientCreationService | transactional Prisma+FHIR+MRN creation |
| 2 | PatientCrudService | FHIR read/update/delete; IFhirClient |
| 3 | PatientSearchService | fuzzy search across Prisma + FHIR |
| 4 | PatientAggregationService | parallel FHIR fetch → PatientSummaryResponseDto |
| 5 | PatientIdResolverService | UUID ↔ FHIR Patient numeric ID resolution |
| 6 | PatientIdResolver (sync) | fast local cache for ID resolution |
| 7 | PatientMedicationService | patient-level medication management |
| 8 | PatientAttachmentService | document/file attachments |
| 9 | PatientCareTeamService | FHIR CareTeam resources |
| 10 | PatientFavoritesActivity | favorite/recently-viewed flags |
| 11 | PatientEverythingService | FHIR $everything export |
PatientCreationService creation steps (9 phases, in order):
| # | Step | Detail |
|---|---|---|
| 1 | findExistingPatient() | dedup check by email hash |
| 2 | generateMrn() | MrnService.assignMrn() |
| 3 | createFhirPatient() | POST Patient to HAPI |
| 4 | createFhirPerson() | POST Person (identity link for MDM) |
| 5 | buildPersonLink() | link Person → Patient |
| 6 | persistDbState() | Prisma User + OrganizationMember + fhir_resource_link |
| 7 | createRelatedPersons() | FHIR RelatedPerson for emergency contacts (with retry backoff) |
| 8 | toEmergencyContactCreate() | Prisma EmergencyContact rows |
| 9 | toInsuranceCoverageCreate() | Prisma InsuranceCoverage rows |
If any HAPI write fails, compensateFhirCreate() deletes the Patient and Person resources to prevent orphaned FHIR data.
See coding_context/ddx-hms-context.md §Clinical for the module map.
Tech Stack & Choices
| Layer | Technology | Rationale |
|---|---|---|
| API | NestJS 11, multi-controller split (patients-crud, patients-documents, patients-medical, etc.) | Split by concern to keep controller files under 700 lines |
| Auth | Role-based: DOCTOR/NURSE for clinical read; RECEPTIONIST/CLINIC_ADMIN for registration; PATIENT for self-service | Fine-grained role gating per endpoint |
| FHIR identity | Patient + Person (MDM) | Person links allows future MDM merge/unmerge without deleting Patient resources |
| MRN | MrnService.assignMrn() — MRN_SYSTEM_PREFIX constant | Format {prefix}-{orgSlug}-{seq} configured per organization |
| Prisma identity | User table (ddx_api_main) | Stores email hash for dedup, fhirPatientId for cross-store resolution, auth credentials |
| ID resolution | PatientIdResolverService — cache + Prisma lookup | UUID → FHIR ID in ~1ms (cache hit) or ~5ms (DB lookup); critical for all FHIR operations |
| Demographics | FHIR Patient.name, .telecom, .address, .gender, .birthDate | Normalized via normalizeGender() + formatFhirAddress() helper functions |
| Fuzzy search | PatientSearchService — Prisma fulltext + FHIR text search | Returns ranked results across both stores; used by receptionist appointment booking |
| Text aggregation | PatientTextAggregationService — builds LLM-ready patient text | Used by TUCAN AI to inject patient context without a separate API call |
Key design decision: The Prisma User UUID is the stable public API identifier — NOT the FHIR Patient numeric ID. All endpoint routes use UUIDs (/patients/:id). The PatientIdResolverService maintains a cache of UUID → FHIR numeric ID mappings to avoid per-request HAPI lookups. Passing a FHIR numeric ID to any patient endpoint returns 404.
Data Flow
New patient registration
Business outcome: Receptionist registers a new patient; within 3 seconds, the patient appears in the system with an MRN and a FHIR Patient record.
Technical mechanism:
POST /api/v1/patientswith{ firstName, lastName, email, birthDate, gender, phone, address, emergencyContacts[], insuranceCoverages[] }andX-Clinic-ID.PatientsCrudController.createPatient()(patients-crud.controller.ts:322) delegates toPatientsService.createPatient().PatientCreationService.createPatientTransactional()(patient-creation.service.ts) executes 9 phases:- Phase 1:
findExistingPatient()byemailHash— throwsConflictExceptionif duplicate. - Phase 2:
generateMrn()—MrnService.assignMrn(orgId)→ MRN string. - Phase 3:
createFhirPatient()→fhirClient.create('Patient', { name, telecom, address, gender, birthDate }, clinicSlug). - Phase 4:
createFhirPerson()→fhirClient.create('Person', { ... }, clinicSlug)for MDM identity linkage. - Phase 6:
persistDbState()→prisma.user.create(...)withfhirPatientIdfrom phase 3 result;prisma.organizationMember.create(...). - Phases 7-9: Create related persons, emergency contacts, insurance coverages.
- Phase 1:
- On any HAPI error:
compensateFhirCreate()deletes Patient + Person resources. - Returns
PatientResponseDtowith UUID, MRN, FHIR Patient ID.
Find patient by fuzzy name
GET /api/v1/patients?search={term}&limit=20 → PatientsService.listPatientsWithFuzzySearch() (patients.service.ts:161) → PatientSearchService queries Prisma fulltext index on firstName + lastName + email, then enriches with FHIR demographics. Returns { data, total, page }.
Get patient detail (medical card)
GET /api/v1/patients/:id → PatientsService.getPatientDetail() (patients.service.ts:217) → PatientAggregationService.getPatientDetail() → parallel FHIR fetch for demographics, conditions, medications, allergies, encounters, appointments → returns PatientDetailResponseDto.
FHIR $everything export
GET /api/v1/patients/:id/everything → PatientsService.getPatientEverything() (patients.service.ts:659) → PatientEverythingService → fhirClient.everything(fhirPatientId, clinicSlug) → FHIR Bundle with all linked resources.
Implicated Code
ddx-api/src/clinical/patients/controllers/patients-crud.controller.ts—PatientsCrudController—@ResourceMetadecorated; handles list/get/create/update/patch/delete/export/bulk/aggregate +getAdjacentPatients,getBatchInfo,backfillLastActivity,regeneratePatientAggregateddx-api/src/clinical/patients/controllers/patients-crud.controller.ts:147—listPatients()—GET /patients— pagination, filters, fuzzy search dispatchddx-api/src/clinical/patients/controllers/patients-crud.controller.ts:302—getPatientDetail()—GET /patients/:id— aggregated medical-card detailddx-api/src/clinical/patients/controllers/patients-crud.controller.ts:347—createPatient()—POST /patients— transactional creation with emergency contacts + insuranceddx-api/src/clinical/patients/controllers/patients-crud.controller.ts:462—patchPatient()—PATCH /patients/:id— partial Prisma + FHIR update;updatePatient()(:420) = full PUT replaceddx-api/src/clinical/patients/controllers/patients-medical.controller.ts:66—getPatientSummary()—GET /patients/:id/summary—PatientSummaryResponseDto(demographics incl. maritalStatus/preferredLanguage/emergencyContacts, conditions, medications, allergies, encounters, appointments)ddx-api/src/clinical/patients/patients.service.ts:93—PatientsService— orchestrator delegating to 11 sub-servicesddx-api/src/clinical/patients/patients.service.ts:161—listPatientsWithFuzzySearch()— fuzzy name/email search across Prisma + FHIRddx-api/src/clinical/patients/patients.service.ts:411—ensureFhirPatient()— lazy FHIR Patient creation for patients withoutfhirPatientId(pre-migration records)ddx-api/src/clinical/patients/services/patient-creation.service.ts:1—PatientCreationService— 9-phase transactional creation with FHIR compensation; MRN assignment; RelatedPerson retry backoff (RELATED_PERSON_BACKOFF_MS,RELATED_PERSON_MAX_ATTEMPTS)ddx-api/src/clinical/patients/services/patient-id-resolver.service.ts:1—PatientIdResolverService— UUID ↔ FHIR Patient ID resolution with sync throttle (SYNC_THROTTLE_MS);resolveFhirPatientId()used by every FHIR-writing serviceddx-api/src/clinical/patients/services/patient-aggregation.service.ts:72—PatientAggregationService— parallel FHIR fetch for medical card; see Medical Cardsddx-api/src/clinical/patients/services/patient-search.service.ts:1—PatientSearchService— Prisma fulltext + FHIR text search; ranked mergeddx-api/src/clinical/patients/dto/patient-summary-response.dto.ts:172—PatientSummaryResponseDto— 6-component aggregate DTO (demographics, conditions, medications, allergies, encounters, appointments)ddx-api/src/platform/mrn/mrn.service.ts:1—MrnService— MRN generation withMRN_SYSTEM_PREFIX; org-scoped sequential assignment
Operational Notes
ensureFhirPatient()is critical: Any patient created before the FHIR sync (or via seeder without FHIR write) will havefhirPatientId = null.PatientsService.ensureFhirPatient()(patients.service.ts:411) creates the FHIR Patient lazily on the next read. This is called byGET /visits/mebefore returning visit history. Services that assumefhirPatientIdis non-null will throwNotFoundExceptionfor pre-migration patients.- UUID is the stable identifier: FHIR Patient numeric IDs change if HAPI is wiped and re-seeded. The Prisma UUID (
user.id) never changes. External systems must use UUIDs in API calls. - Email hash dedup:
PatientCreationService.findExistingPatient()checksemailHash(SHA-256 of lowercase email) — not raw email — to avoid storing PII in the lookup index. Registering the same patient email twice throwsConflictException. - RelatedPerson retry: HAPI sometimes returns a transient error when creating RelatedPerson resources immediately after Person creation.
createOneRelatedPersonWithRetry()uses exponential backoff (RELATED_PERSON_BACKOFF_MS,RELATED_PERSON_MAX_ATTEMPTS) before giving up (non-fatal — patient is still created). - Patch vs PUT semantics:
PATCH /patients/:idis a partial update (only supplied fields are changed in Prisma + FHIR).PUT /patients/:idis a full replace. Use PATCH for UI edit forms; PUT for bulk import. - FHIR patient ID resolution cache:
PatientIdResolverServicecaches UUID→FHIR ID mappings. If a FHIR Patient is deleted and re-created (e.g., after a HAPI reset in dev), the cache must be cleared or the service restarted to pick up the new FHIR IDs.
Related Topics
- Medical Cards — Patient Health Summary — PatientAggregationService aggregates patient FHIR data into dashboard cards
- Clinical Visits — Visits (Encounters) are linked to patients via
subject.reference - Emergency Contacts and Patient Relations — EmergencyContact and RelatedPerson resources created at registration time
- Clinical Notes — DocumentReference resources linked to patient via
subject.reference - Lab Results and Diagnostic Reports — DiagnosticReport resources linked via
subject.reference - HAPI FHIR R4 Server — FHIR Patient storage, partition routing, $everything operation
- FHIR Partitions and Multi-Tenancy — Clinic partition isolation for patient data