EHR & Patient Management Data Specifications
This document defines the complete data model for the EHR & Patient Management module. As the owning module for core shared entities (Patients, Providers, Users & Auth, Facilities), all canonical table definitions reside here. Other modules reference these tables via foreign keys.
UAE Regulatory Context: All data structures comply with UAE PDPL (Federal Decree-Law No. 45/2021) requirements for sensitive health data processing, including audit trail immutability, consent tracking, and data subject rights support. Field-level encryption is required for Emirates ID and other national identifiers per NESA/ADHICS standards.
Shared Entity References
This module owns the following shared entities referenced by all other modules:
| Shared Entity | Tables | Referenced By (Other Modules) |
|---|---|---|
| Patients | patients, patient_demographics, patient_identifiers |
All modules via FK to patients.patient_id |
| Providers | providers, provider_credentials |
CPOE, RIS, LIS, PIS, Scheduling, Portals via FK to providers.provider_id |
| Users & Auth | users, roles, permissions, role_permissions, user_roles |
All modules via FK to users.user_id |
| Facilities | facilities, departments, locations |
All modules via FK to facilities.facility_id, departments.department_id, locations.location_id |
This module references the following shared entities from other modules:
| Shared Entity | Owning Module | FK Used |
|---|---|---|
| Encounters | scheduling | encounters.encounter_id |
| Payers | policy-contract-mgmt | payers.payer_id |
Entity Relationship Diagram
Table Definitions
patients
Purpose: Core patient identity record. Single row per unique patient. Serves as the canonical patient reference for the entire HIS.
| Field Name | Data Type | Length | Nullable | Default | Validation | Description |
|---|---|---|---|---|---|---|
| patient_id | BIGINT | — | NO | AUTO_INCREMENT | — | Primary key |
| mrn | VARCHAR | 20 | NO | — | UNIQUE; facility-prefix + sequence | Medical Record Number |
| first_name_en | VARCHAR | 100 | NO | — | Non-empty | Patient first/given name (English) |
| middle_name_en | VARCHAR | 100 | YES | NULL | — | Middle name (English) |
| last_name_en | VARCHAR | 100 | NO | — | Non-empty | Family/surname (English) |
| first_name_ar | VARCHAR | 100 | YES | NULL | Arabic script validation | First name (Arabic) |
| middle_name_ar | VARCHAR | 100 | YES | NULL | — | Middle name (Arabic) |
| last_name_ar | VARCHAR | 100 | YES | NULL | — | Family name (Arabic) |
| date_of_birth | DATE | — | NO | — | <= CURRENT_DATE; plausibility check (not >150 years ago) | Date of birth |
| gender | VARCHAR | 10 | NO | — | IN ('male', 'female', 'unknown') | Administrative gender |
| nationality_code | VARCHAR | 3 | YES | NULL | FK to ref_nationalities.nationality_code | ISO 3166-1 alpha-3 nationality |
| is_vip | BOOLEAN | — | NO | FALSE | — | VIP flag (restricts access to authorized users only) |
| is_active | BOOLEAN | — | NO | TRUE | — | Active patient record (FALSE if merged or deceased) |
| is_deceased | BOOLEAN | — | NO | FALSE | — | Deceased indicator |
| deceased_datetime | TIMESTAMP | — | YES | NULL | Required if is_deceased = TRUE | Date/time of death |
| merged_into_patient_id | BIGINT | — | YES | NULL | FK to patients.patient_id (self-ref) | If merged, points to surviving record |
| blood_group | VARCHAR | 5 | YES | NULL | IN ('A+','A-','B+','B-','AB+','AB-','O+','O-') | ABO/Rh blood group |
| preferred_language | VARCHAR | 5 | YES | 'en' | BCP-47 code | Preferred communication language |
| photo_url | VARCHAR | 500 | YES | NULL | Valid URL or storage path | Patient photograph path |
| created_by | BIGINT | — | NO | — | FK to users.user_id | User who created record |
| updated_by | BIGINT | — | YES | NULL | FK to users.user_id | User who last updated |
| created_at | TIMESTAMP | — | NO | CURRENT_TIMESTAMP | — | Record creation timestamp |
| updated_at | TIMESTAMP | — | NO | CURRENT_TIMESTAMP ON UPDATE | — | Record last update timestamp |
Indexes:
CREATE UNIQUE INDEX idx_patients_mrn ON patients(mrn);
CREATE INDEX idx_patients_name_en ON patients(last_name_en, first_name_en);
CREATE INDEX idx_patients_name_ar ON patients(last_name_ar, first_name_ar);
CREATE INDEX idx_patients_dob ON patients(date_of_birth);
CREATE INDEX idx_patients_active ON patients(is_active) WHERE is_active = TRUE;
CREATE INDEX idx_patients_merged ON patients(merged_into_patient_id) WHERE merged_into_patient_id IS NOT NULL;
FHIR Mapping:
- FHIR Resource:
Patient - Key mappings:
patient_id→idmrn→identifier(type MR, system = facility OID)first_name_en/last_name_en→name[use=official].given/name.familyfirst_name_ar/last_name_ar→name[use=official, extension=language:ar]date_of_birth→birthDategender→genderis_deceased/deceased_datetime→deceasedBoolean/deceasedDateTimeis_active→activemerged_into_patient_id→link[type=replaced-by]preferred_language→communication.language
patient_demographics
Purpose: Versioned demographic snapshots (SCD Type 2). Each update creates a new row; previous rows retained for audit history. Only one row per patient has is_current = TRUE.
| Field Name | Data Type | Length | Nullable | Default | Validation | Description |
|---|---|---|---|---|---|---|
| demographic_id | BIGINT | — | NO | AUTO_INCREMENT | — | Primary key |
| patient_id | BIGINT | — | NO | — | FK to patients.patient_id | Patient reference |
| is_current | BOOLEAN | — | NO | TRUE | Only one TRUE per patient_id | Current active version |
| effective_from | TIMESTAMP | — | NO | CURRENT_TIMESTAMP | — | Version effective start |
| effective_to | TIMESTAMP | — | YES | NULL | NULL = current version | Version effective end |
| address_line1_en | VARCHAR | 200 | YES | NULL | — | Street address line 1 (English) |
| address_line2_en | VARCHAR | 200 | YES | NULL | — | Street address line 2 (English) |
| address_line1_ar | VARCHAR | 200 | YES | NULL | — | Street address line 1 (Arabic) |
| po_box | VARCHAR | 20 | YES | NULL | — | PO Box number |
| emirate_code | VARCHAR | 3 | YES | NULL | FK to ref_emirates_cities.emirate_code | Emirate of residence |
| city | VARCHAR | 100 | YES | NULL | — | City of residence |
| country_code | VARCHAR | 3 | YES | 'ARE' | ISO 3166-1 alpha-3 | Country of residence |
| mobile_phone | VARCHAR | 20 | YES | NULL | E.164 format (+971XXXXXXXXX) | Primary mobile number |
| home_phone | VARCHAR | 20 | YES | NULL | E.164 format | Home telephone |
| work_phone | VARCHAR | 20 | YES | NULL | E.164 format | Work telephone |
| VARCHAR | 200 | YES | NULL | Valid email format | Email address | |
| marital_status | VARCHAR | 20 | YES | NULL | IN ('single','married','divorced','widowed','separated','unknown') | Marital status |
| occupation | VARCHAR | 100 | YES | NULL | — | Current occupation |
| employer_name | VARCHAR | 200 | YES | NULL | — | Employer name |
| religion | VARCHAR | 50 | YES | NULL | — | Religion (optional, per patient consent) |
| created_by | BIGINT | — | NO | — | FK to users.user_id | User who created version |
| created_at | TIMESTAMP | — | NO | CURRENT_TIMESTAMP | — | Version creation timestamp |
Indexes:
CREATE INDEX idx_patient_demo_patient ON patient_demographics(patient_id);
CREATE UNIQUE INDEX idx_patient_demo_current ON patient_demographics(patient_id) WHERE is_current = TRUE;
CREATE INDEX idx_patient_demo_emirate ON patient_demographics(emirate_code);
CREATE INDEX idx_patient_demo_mobile ON patient_demographics(mobile_phone);
CREATE INDEX idx_patient_demo_email ON patient_demographics(email);
FHIR Mapping:
- Maps to
Patientresource fields: address_line1_en,city,emirate_code,country_code→addressmobile_phone,home_phone,email→telecommarital_status→maritalStatus
patient_identifiers
Purpose: Stores all patient identifiers (MRN, Emirates ID, passport, visa number, insurance member ID). Supports multiple identifier types per patient with primary/secondary flags and expiry tracking.
| Field Name | Data Type | Length | Nullable | Default | Validation | Description |
|---|---|---|---|---|---|---|
| identifier_id | BIGINT | — | NO | AUTO_INCREMENT | — | Primary key |
| patient_id | BIGINT | — | NO | — | FK to patients.patient_id | Patient reference |
| identifier_type | VARCHAR | 30 | NO | — | IN ('MRN','EMIRATES_ID','PASSPORT','VISA','INSURANCE_MEMBER_ID','OLD_MRN','NATIONAL_ID','OTHER') | Type of identifier |
| identifier_value | VARCHAR | 100 | NO | — | Non-empty | Identifier value |
| identifier_system | VARCHAR | 200 | YES | NULL | — | Issuing system/authority URI |
| is_primary | BOOLEAN | — | NO | TRUE | — | Primary identifier for this type |
| verification_status | VARCHAR | 20 | NO | 'unverified' | IN ('verified','unverified','expired','invalid') | Verification status |
| issue_date | DATE | — | YES | NULL | — | Date identifier was issued |
| expiry_date | DATE | — | YES | NULL | >= issue_date | Expiry date (passports, visas, insurance) |
| issuing_authority | VARCHAR | 100 | YES | NULL | — | Issuing authority (e.g., MOI, embassy) |
| created_by | BIGINT | — | NO | — | FK to users.user_id | User who created record |
| created_at | TIMESTAMP | — | NO | CURRENT_TIMESTAMP | — | Record creation timestamp |
| updated_at | TIMESTAMP | — | NO | CURRENT_TIMESTAMP ON UPDATE | — | Last update timestamp |
Indexes:
CREATE INDEX idx_patient_idents_patient ON patient_identifiers(patient_id);
CREATE INDEX idx_patient_idents_type_value ON patient_identifiers(identifier_type, identifier_value);
CREATE UNIQUE INDEX idx_patient_idents_eid ON patient_identifiers(identifier_value) WHERE identifier_type = 'EMIRATES_ID' AND is_primary = TRUE;
CREATE INDEX idx_patient_idents_expiry ON patient_identifiers(expiry_date) WHERE expiry_date IS NOT NULL;
Constraints:
ALTER TABLE patient_identifiers
ADD CONSTRAINT fk_ident_patient FOREIGN KEY (patient_id) REFERENCES patients(patient_id),
ADD CONSTRAINT chk_eid_format CHECK (
identifier_type != 'EMIRATES_ID' OR identifier_value ~ '^784-[0-9]{4}-[0-9]{7}-[0-9]$'
);
FHIR Mapping:
- Maps to
Patient.identifier[]: identifier_type→identifier.type.coding.code(MR, NI, PPN, etc.)identifier_value→identifier.valueidentifier_system→identifier.systemexpiry_date→identifier.period.end
patient_contacts
Purpose: Emergency contacts and next-of-kin for each patient.
| Field Name | Data Type | Length | Nullable | Default | Validation | Description |
|---|---|---|---|---|---|---|
| contact_id | BIGINT | — | NO | AUTO_INCREMENT | — | Primary key |
| patient_id | BIGINT | — | NO | — | FK to patients.patient_id | Patient reference |
| relationship | VARCHAR | 30 | NO | — | IN ('spouse','parent','child','sibling','guardian','friend','employer','other') | Relationship to patient |
| contact_name_en | VARCHAR | 200 | NO | — | Non-empty | Contact full name (English) |
| contact_name_ar | VARCHAR | 200 | YES | NULL | — | Contact full name (Arabic) |
| mobile_phone | VARCHAR | 20 | NO | — | E.164 format | Primary phone number |
| home_phone | VARCHAR | 20 | YES | NULL | — | Home phone |
| VARCHAR | 200 | YES | NULL | Valid email format | Email address | |
| is_emergency_contact | BOOLEAN | — | NO | TRUE | — | Is emergency contact |
| is_next_of_kin | BOOLEAN | — | NO | FALSE | — | Is legal next of kin |
| priority | INT | — | NO | 1 | >= 1 | Contact priority order |
| created_by | BIGINT | — | NO | — | FK to users.user_id | Created by |
| created_at | TIMESTAMP | — | NO | CURRENT_TIMESTAMP | — | Created timestamp |
| updated_at | TIMESTAMP | — | NO | CURRENT_TIMESTAMP ON UPDATE | — | Updated timestamp |
Indexes:
CREATE INDEX idx_patient_contacts_patient ON patient_contacts(patient_id);
CREATE INDEX idx_patient_contacts_emergency ON patient_contacts(patient_id, is_emergency_contact) WHERE is_emergency_contact = TRUE;
FHIR Mapping:
- Maps to
Patient.contact[]: relationship→contact.relationship.codingcontact_name_en→contact.namemobile_phone→contact.telecom
patient_allergies
Purpose: Stores patient allergy and adverse reaction records. Supports coded allergens (RxNorm for drugs, SNOMED for non-drug allergens), reaction types, and severity grading. NKA/NKDA status captured as special entries.
| Field Name | Data Type | Length | Nullable | Default | Validation | Description |
|---|---|---|---|---|---|---|
| allergy_id | BIGINT | — | NO | AUTO_INCREMENT | — | Primary key |
| patient_id | BIGINT | — | NO | — | FK to patients.patient_id | Patient reference |
| encounter_id | BIGINT | — | YES | NULL | FK to encounters.encounter_id | Encounter when documented |
| allergen_type | VARCHAR | 20 | NO | — | IN ('medication','food','environmental','biologic','other') | Category of allergen |
| allergen_description | VARCHAR | 300 | NO | — | Non-empty | Free-text allergen name |
| allergen_code | VARCHAR | 20 | YES | NULL | — | Coded allergen identifier |
| allergen_system | VARCHAR | 50 | YES | NULL | IN ('RxNorm','SNOMED','NDFRT','FDB') when coded | Coding system for allergen_code |
| reaction_description | VARCHAR | 500 | YES | NULL | — | Description of reaction |
| reaction_code | VARCHAR | 20 | YES | NULL | SNOMED CT code | Coded reaction (e.g., urticaria, anaphylaxis) |
| severity | VARCHAR | 20 | NO | 'moderate' | IN ('mild','moderate','severe','life-threatening') | Reaction severity |
| criticality | VARCHAR | 10 | YES | NULL | IN ('low','high','unable-to-assess') | Clinical criticality assessment |
| status | VARCHAR | 20 | NO | 'active' | IN ('active','inactive','resolved','entered-in-error','nka','nkda') | Allergy record status |
| onset_date | DATE | — | YES | NULL | — | Approximate onset date |
| verification_status | VARCHAR | 20 | NO | 'unconfirmed' | IN ('unconfirmed','confirmed','refuted','entered-in-error') | Verification status |
| entered_by | BIGINT | — | NO | — | FK to users.user_id | Clinician who documented |
| verified_by | BIGINT | — | YES | NULL | FK to users.user_id | Clinician who verified |
| verified_at | TIMESTAMP | — | YES | NULL | — | Verification timestamp |
| notes | TEXT | — | YES | NULL | — | Additional clinical notes |
| created_at | TIMESTAMP | — | NO | CURRENT_TIMESTAMP | — | Record creation timestamp |
| updated_at | TIMESTAMP | — | NO | CURRENT_TIMESTAMP ON UPDATE | — | Last update timestamp |
Indexes:
CREATE INDEX idx_allergies_patient ON patient_allergies(patient_id);
CREATE INDEX idx_allergies_status ON patient_allergies(patient_id, status) WHERE status = 'active';
CREATE INDEX idx_allergies_allergen ON patient_allergies(allergen_code, allergen_system);
CREATE INDEX idx_allergies_encounter ON patient_allergies(encounter_id);
Constraints:
ALTER TABLE patient_allergies
ADD CONSTRAINT fk_allergy_patient FOREIGN KEY (patient_id) REFERENCES patients(patient_id),
ADD CONSTRAINT fk_allergy_encounter FOREIGN KEY (encounter_id) REFERENCES encounters(encounter_id),
ADD CONSTRAINT fk_allergy_entered_by FOREIGN KEY (entered_by) REFERENCES users(user_id),
ADD CONSTRAINT chk_nka_no_allergen CHECK (
status NOT IN ('nka','nkda') OR (allergen_code IS NULL AND allergen_description = status)
);
Terminology Binding:
- RxNorm: Drug allergens (
allergen_codewhenallergen_system = 'RxNorm') - SNOMED CT: Non-drug allergens and reaction codes (
allergen_codewhenallergen_system = 'SNOMED';reaction_code) - FHIR ValueSet:
http://hl7.org/fhir/ValueSet/allergyintolerance-code
FHIR Mapping:
- FHIR Resource:
AllergyIntolerance - Key mappings:
allergy_id→idpatient_id→patient.referenceallergen_code+allergen_system→code.codingallergen_type→categoryseverity/criticality→reaction.severity/criticalitystatus→clinicalStatusverification_status→verificationStatus
patient_problems
Purpose: Patient problem list with ICD-10-AM and SNOMED CT coding. Tracks active, resolved, and chronic conditions with encounter linkage.
| Field Name | Data Type | Length | Nullable | Default | Validation | Description |
|---|---|---|---|---|---|---|
| problem_id | BIGINT | — | NO | AUTO_INCREMENT | — | Primary key |
| patient_id | BIGINT | — | NO | — | FK to patients.patient_id | Patient reference |
| encounter_id | BIGINT | — | YES | NULL | FK to encounters.encounter_id | Encounter when documented |
| problem_description | VARCHAR | 500 | NO | — | Non-empty | Free-text problem description |
| icd10_code | VARCHAR | 10 | YES | NULL | Valid ICD-10-AM format | ICD-10-AM diagnosis code |
| icd10_description | VARCHAR | 300 | YES | NULL | — | ICD-10-AM code description |
| snomed_code | VARCHAR | 20 | YES | NULL | Valid SNOMED CT concept ID | SNOMED CT concept code |
| snomed_description | VARCHAR | 300 | YES | NULL | — | SNOMED CT preferred term |
| problem_type | VARCHAR | 20 | NO | 'problem' | IN ('problem','diagnosis','symptom','finding','complaint') | Problem classification |
| status | VARCHAR | 20 | NO | 'active' | IN ('active','resolved','inactive','recurrence','remission') | Clinical status |
| severity | VARCHAR | 20 | YES | NULL | IN ('mild','moderate','severe') | Problem severity |
| onset_date | DATE | — | YES | NULL | — | Approximate onset date |
| resolved_date | DATE | — | YES | NULL | >= onset_date; required if status = 'resolved' | Date problem resolved |
| is_chronic | BOOLEAN | — | NO | FALSE | — | Chronic condition flag |
| is_principal | BOOLEAN | — | NO | FALSE | — | Principal diagnosis for encounter |
| entered_by | BIGINT | — | NO | — | FK to users.user_id | Clinician who documented |
| verified_by | BIGINT | — | YES | NULL | FK to users.user_id | Clinician who verified |
| notes | TEXT | — | YES | NULL | — | Additional clinical notes |
| created_at | TIMESTAMP | — | NO | CURRENT_TIMESTAMP | — | Record creation timestamp |
| updated_at | TIMESTAMP | — | NO | CURRENT_TIMESTAMP ON UPDATE | — | Last update timestamp |
Indexes:
CREATE INDEX idx_problems_patient ON patient_problems(patient_id);
CREATE INDEX idx_problems_active ON patient_problems(patient_id, status) WHERE status = 'active';
CREATE INDEX idx_problems_icd10 ON patient_problems(icd10_code);
CREATE INDEX idx_problems_snomed ON patient_problems(snomed_code);
CREATE INDEX idx_problems_encounter ON patient_problems(encounter_id);
CREATE INDEX idx_problems_chronic ON patient_problems(patient_id, is_chronic) WHERE is_chronic = TRUE;
Terminology Binding:
- ICD-10-AM (Australian Modification, adopted by UAE): Primary diagnosis coding (
icd10_code) - SNOMED CT: Clinical terminology for problem description (
snomed_code) - FHIR ValueSet:
http://hl7.org/fhir/ValueSet/condition-code
FHIR Mapping:
- FHIR Resource:
Condition - Key mappings:
problem_id→idpatient_id→subject.referenceicd10_code→code.coding[system=http://hl7.org/fhir/sid/icd-10]snomed_code→code.coding[system=http://snomed.info/sct]status→clinicalStatusonset_date→onsetDateTimeresolved_date→abatementDateTimeis_principal→category(encounter-diagnosis)
patient_consents
Purpose: Tracks all patient consents as required by UAE PDPL. Covers treatment consent, data processing, HIE sharing (NABIDH/Malaffi), research participation, and portal registration. Immutable once signed — amendments create new rows.
| Field Name | Data Type | Length | Nullable | Default | Validation | Description |
|---|---|---|---|---|---|---|
| consent_id | BIGINT | — | NO | AUTO_INCREMENT | — | Primary key |
| patient_id | BIGINT | — | NO | — | FK to patients.patient_id | Patient reference |
| encounter_id | BIGINT | — | YES | NULL | FK to encounters.encounter_id | Encounter context (if applicable) |
| consent_type_code | VARCHAR | 50 | NO | — | FK to ref_consent_types.consent_type_code | Type of consent |
| consent_template_id | BIGINT | — | YES | NULL | FK to ref_consent_templates.template_id | Template version used |
| status | VARCHAR | 20 | NO | — | IN ('granted','declined','withdrawn','expired') | Consent status |
| processing_legal_basis | VARCHAR | 30 | NO | — | IN ('explicit_consent','treatment_exemption','public_health','legal_obligation','vital_interest','contractual') | UAE PDPL legal basis |
| granted_datetime | TIMESTAMP | — | YES | NULL | — | When consent was granted |
| withdrawn_datetime | TIMESTAMP | — | YES | NULL | >= granted_datetime | When consent was withdrawn |
| expiry_datetime | TIMESTAMP | — | YES | NULL | >= granted_datetime | Consent expiry (if time-limited) |
| signature_method | VARCHAR | 30 | YES | NULL | IN ('digital_pad','portal_click','verbal_witnessed','paper_scanned') | How consent was captured |
| signature_data | TEXT | — | YES | NULL | — | Digital signature blob or reference |
| document_id | BIGINT | — | YES | NULL | FK to patient_documents.document_id | Signed consent document |
| witnessed_by | BIGINT | — | YES | NULL | FK to users.user_id | Witness user ID |
| collected_by | BIGINT | — | NO | — | FK to users.user_id | User who collected consent |
| notes | TEXT | — | YES | NULL | — | Additional notes |
| created_at | TIMESTAMP | — | NO | CURRENT_TIMESTAMP | — | Record creation timestamp |
Indexes:
CREATE INDEX idx_consents_patient ON patient_consents(patient_id);
CREATE INDEX idx_consents_type ON patient_consents(patient_id, consent_type_code);
CREATE INDEX idx_consents_status ON patient_consents(status);
CREATE INDEX idx_consents_expiry ON patient_consents(expiry_datetime) WHERE expiry_datetime IS NOT NULL AND status = 'granted';
Constraints:
ALTER TABLE patient_consents
ADD CONSTRAINT fk_consent_patient FOREIGN KEY (patient_id) REFERENCES patients(patient_id),
ADD CONSTRAINT fk_consent_encounter FOREIGN KEY (encounter_id) REFERENCES encounters(encounter_id),
ADD CONSTRAINT chk_granted_datetime CHECK (status != 'granted' OR granted_datetime IS NOT NULL),
ADD CONSTRAINT chk_withdrawn_datetime CHECK (status != 'withdrawn' OR withdrawn_datetime IS NOT NULL);
FHIR Mapping:
- FHIR Resource:
Consent - Key mappings:
consent_id→idpatient_id→patient.referencestatus→status(active | rejected | inactive)consent_type_code→category.codinggranted_datetime/expiry_datetime→provision.periodprocessing_legal_basis→policyRule
patient_documents
Purpose: Metadata for all patient-related documents — scanned IDs, insurance cards, signed consents, clinical documents, discharge summaries. Actual file content stored in document management system (DMS); this table holds metadata and storage references.
| Field Name | Data Type | Length | Nullable | Default | Validation | Description |
|---|---|---|---|---|---|---|
| document_id | BIGINT | — | NO | AUTO_INCREMENT | — | Primary key |
| patient_id | BIGINT | — | NO | — | FK to patients.patient_id | Patient reference |
| encounter_id | BIGINT | — | YES | NULL | FK to encounters.encounter_id | Associated encounter |
| document_type_code | VARCHAR | 50 | NO | — | FK to ref_document_types.document_type_code | Document category |
| document_title | VARCHAR | 300 | NO | — | Non-empty | Document title/description |
| mime_type | VARCHAR | 100 | NO | — | Valid MIME type | File MIME type (application/pdf, image/jpeg, etc.) |
| file_size_bytes | BIGINT | — | YES | NULL | > 0 | File size in bytes |
| storage_path | VARCHAR | 500 | NO | — | Non-empty | DMS storage path or object key |
| storage_system | VARCHAR | 30 | NO | 'dms' | IN ('dms','s3','azure_blob','local') | Storage backend |
| status | VARCHAR | 20 | NO | 'active' | IN ('active','archived','deleted','pending_scan') | Document status |
| uploaded_by | BIGINT | — | NO | — | FK to users.user_id | User who uploaded |
| upload_datetime | TIMESTAMP | — | NO | CURRENT_TIMESTAMP | — | Upload timestamp |
| retention_expiry | DATE | — | YES | NULL | — | Retention period expiry (per UAE regulations) |
| checksum_sha256 | VARCHAR | 64 | YES | NULL | — | File integrity hash |
| created_at | TIMESTAMP | — | NO | CURRENT_TIMESTAMP | — | Record creation |
| updated_at | TIMESTAMP | — | NO | CURRENT_TIMESTAMP ON UPDATE | — | Last update |
Indexes:
CREATE INDEX idx_documents_patient ON patient_documents(patient_id);
CREATE INDEX idx_documents_encounter ON patient_documents(encounter_id);
CREATE INDEX idx_documents_type ON patient_documents(document_type_code);
CREATE INDEX idx_documents_status ON patient_documents(status);
CREATE INDEX idx_documents_retention ON patient_documents(retention_expiry) WHERE retention_expiry IS NOT NULL;
FHIR Mapping:
- FHIR Resource:
DocumentReference - Key mappings:
document_id→idpatient_id→subject.referencedocument_type_code→type.codingstatus→statusmime_type→content.attachment.contentTypestorage_path→content.attachment.url
clinical_notes
Purpose: All clinical documentation — H&P notes, progress notes, discharge summaries, consultation notes, nursing assessments. Supports structured templates, co-signing, and addenda.
| Field Name | Data Type | Length | Nullable | Default | Validation | Description |
|---|---|---|---|---|---|---|
| note_id | BIGINT | — | NO | AUTO_INCREMENT | — | Primary key |
| patient_id | BIGINT | — | NO | — | FK to patients.patient_id | Patient reference |
| encounter_id | BIGINT | — | NO | — | FK to encounters.encounter_id | Encounter context |
| note_type | VARCHAR | 30 | NO | — | IN ('H_AND_P','PROGRESS','DISCHARGE','CONSULT','NURSING_ASSESSMENT','PROCEDURE','OPERATIVE','ED','ADDENDUM') | Note category |
| template_id | BIGINT | — | YES | NULL | FK to ref_note_templates.template_id | Template used (if structured) |
| note_title | VARCHAR | 300 | NO | — | Non-empty | Note title |
| note_body | TEXT | — | NO | — | Non-empty | Full text of clinical note |
| structured_data | JSON | — | YES | NULL | — | Structured fields from template |
| status | VARCHAR | 20 | NO | 'draft' | IN ('draft','pending_cosign','signed','amended','entered_in_error') | Note workflow status |
| author_provider_id | BIGINT | — | NO | — | FK to providers.provider_id | Authoring clinician |
| authored_datetime | TIMESTAMP | — | NO | CURRENT_TIMESTAMP | — | When note was authored |
| signed_by | BIGINT | — | YES | NULL | FK to providers.provider_id | Signing provider |
| signed_datetime | TIMESTAMP | — | YES | NULL | >= authored_datetime | When note was signed |
| cosigner_provider_id | BIGINT | — | YES | NULL | FK to providers.provider_id | Co-signer (attendings for residents) |
| cosigned_datetime | TIMESTAMP | — | YES | NULL | >= signed_datetime | When co-signed |
| parent_note_id | BIGINT | — | YES | NULL | FK to clinical_notes.note_id (self-ref) | Parent note (for addenda) |
| specialty_code | VARCHAR | 20 | YES | NULL | FK to ref_provider_specialties.specialty_code | Clinical specialty |
| created_at | TIMESTAMP | — | NO | CURRENT_TIMESTAMP | — | Record creation |
| updated_at | TIMESTAMP | — | NO | CURRENT_TIMESTAMP ON UPDATE | — | Last update |
Indexes:
CREATE INDEX idx_notes_patient ON clinical_notes(patient_id);
CREATE INDEX idx_notes_encounter ON clinical_notes(encounter_id);
CREATE INDEX idx_notes_author ON clinical_notes(author_provider_id);
CREATE INDEX idx_notes_status ON clinical_notes(status);
CREATE INDEX idx_notes_type ON clinical_notes(note_type);
CREATE INDEX idx_notes_pending ON clinical_notes(status, authored_datetime) WHERE status IN ('draft', 'pending_cosign');
CREATE INDEX idx_notes_parent ON clinical_notes(parent_note_id) WHERE parent_note_id IS NOT NULL;
FHIR Mapping:
- FHIR Resource:
DocumentReference(for metadata) /Composition(for structured notes) - Key mappings:
note_id→idpatient_id→subject.referenceencounter_id→context.encounter.referenceauthor_provider_id→author.referencenote_type→type.coding(LOINC document type codes)status→docStatusnote_body→content.attachment
duplicate_suspects
Purpose: Tracks potential duplicate patient records identified by the MPI matching engine or manual flagging. Supports the duplicate resolution workflow (WF-EHRPATIENTMGMT-003).
| Field Name | Data Type | Length | Nullable | Default | Validation | Description |
|---|---|---|---|---|---|---|
| suspect_id | BIGINT | — | NO | AUTO_INCREMENT | — | Primary key |
| patient_id_1 | BIGINT | — | NO | — | FK to patients.patient_id | First patient in suspected pair |
| patient_id_2 | BIGINT | — | NO | — | FK to patients.patient_id; != patient_id_1 | Second patient in suspected pair |
| match_score | DECIMAL | 5,2 | NO | — | 0.00–100.00 | MPI match confidence score |
| match_method | VARCHAR | 30 | NO | — | IN ('deterministic','probabilistic','manual','registration_flag') | How duplicate was detected |
| matching_fields | JSON | — | YES | NULL | — | Fields that matched (e.g., {"emirates_id": true, "dob": true, "name": 0.85}) |
| status | VARCHAR | 20 | NO | 'new' | IN ('new','in_review','pending_approval','resolved','rejected','auto_resolved') | Resolution status |
| resolution | VARCHAR | 20 | YES | NULL | IN ('merged','not_duplicate','deferred') when status = 'resolved' | Resolution outcome |
| primary_patient_id | BIGINT | — | YES | NULL | FK to patients.patient_id | Surviving patient after merge |
| flagged_at | TIMESTAMP | — | NO | CURRENT_TIMESTAMP | — | When duplicate was flagged |
| flagged_by | BIGINT | — | YES | NULL | FK to users.user_id (NULL if system-detected) | User who flagged (if manual) |
| reviewed_by | BIGINT | — | YES | NULL | FK to users.user_id | MRO who reviewed |
| reviewed_at | TIMESTAMP | — | YES | NULL | — | Review timestamp |
| approved_by | BIGINT | — | YES | NULL | FK to users.user_id | HIM Supervisor who approved merge |
| approved_at | TIMESTAMP | — | YES | NULL | — | Approval timestamp |
| notes | TEXT | — | YES | NULL | — | Review notes |
| created_at | TIMESTAMP | — | NO | CURRENT_TIMESTAMP | — | Record creation |
| updated_at | TIMESTAMP | — | NO | CURRENT_TIMESTAMP ON UPDATE | — | Last update |
Indexes:
CREATE INDEX idx_dupes_patient1 ON duplicate_suspects(patient_id_1);
CREATE INDEX idx_dupes_patient2 ON duplicate_suspects(patient_id_2);
CREATE INDEX idx_dupes_status ON duplicate_suspects(status);
CREATE UNIQUE INDEX idx_dupes_pair ON duplicate_suspects(
LEAST(patient_id_1, patient_id_2), GREATEST(patient_id_1, patient_id_2)
) WHERE status NOT IN ('resolved','rejected');
CREATE INDEX idx_dupes_score ON duplicate_suspects(match_score DESC);
providers
Purpose: Registry of all clinical and administrative providers (physicians, nurses, pharmacists, technicians). Canonical definition — all modules reference via FK.
| Field Name | Data Type | Length | Nullable | Default | Validation | Description |
|---|---|---|---|---|---|---|
| provider_id | BIGINT | — | NO | AUTO_INCREMENT | — | Primary key |
| staff_number | VARCHAR | 20 | NO | — | UNIQUE | Internal staff/badge number |
| first_name_en | VARCHAR | 100 | NO | — | Non-empty | First name (English) |
| middle_name_en | VARCHAR | 100 | YES | NULL | — | Middle name (English) |
| last_name_en | VARCHAR | 100 | NO | — | Non-empty | Last name (English) |
| first_name_ar | VARCHAR | 100 | YES | NULL | — | First name (Arabic) |
| last_name_ar | VARCHAR | 100 | YES | NULL | — | Last name (Arabic) |
| provider_type | VARCHAR | 30 | NO | — | IN ('physician','nurse','pharmacist','technician','therapist','admin','other') | Provider category |
| specialty_code | VARCHAR | 20 | YES | NULL | FK to ref_provider_specialties.specialty_code | Primary specialty |
| subspecialty_code | VARCHAR | 20 | YES | NULL | — | Subspecialty if applicable |
| license_number | VARCHAR | 50 | NO | — | Non-empty | Healthcare professional license number |
| licensing_authority | VARCHAR | 10 | NO | — | IN ('DOH','DHA','MOH') | Licensing body |
| license_expiry_date | DATE | — | NO | — | >= CURRENT_DATE for active providers | License expiry date |
| national_id | VARCHAR | 20 | YES | NULL | — | Emirates ID or national ID |
| VARCHAR | 200 | YES | NULL | Valid email | Professional email | |
| mobile_phone | VARCHAR | 20 | YES | NULL | E.164 format | Professional mobile |
| department_id | BIGINT | — | YES | NULL | FK to departments.department_id | Primary department |
| facility_id | BIGINT | — | NO | — | FK to facilities.facility_id | Primary facility |
| is_active | BOOLEAN | — | NO | TRUE | — | Active provider flag |
| doh_provider_id | VARCHAR | 30 | YES | NULL | — | DOH-assigned provider ID (Abu Dhabi) |
| dha_provider_id | VARCHAR | 30 | YES | NULL | — | DHA-assigned provider ID (Dubai) |
| can_prescribe | BOOLEAN | — | NO | FALSE | — | Has prescribing authority |
| can_prescribe_controlled | BOOLEAN | — | NO | FALSE | — | Can prescribe UAE MOH controlled substances |
| digital_signature_cert | VARCHAR | 500 | YES | NULL | — | Digital certificate reference for e-signing |
| created_by | BIGINT | — | NO | — | FK to users.user_id | Created by |
| created_at | TIMESTAMP | — | NO | CURRENT_TIMESTAMP | — | Created timestamp |
| updated_at | TIMESTAMP | — | NO | CURRENT_TIMESTAMP ON UPDATE | — | Updated timestamp |
Indexes:
CREATE UNIQUE INDEX idx_providers_staff ON providers(staff_number);
CREATE INDEX idx_providers_name ON providers(last_name_en, first_name_en);
CREATE INDEX idx_providers_specialty ON providers(specialty_code);
CREATE INDEX idx_providers_license ON providers(license_number, licensing_authority);
CREATE INDEX idx_providers_facility ON providers(facility_id);
CREATE INDEX idx_providers_department ON providers(department_id);
CREATE INDEX idx_providers_active ON providers(is_active) WHERE is_active = TRUE;
CREATE INDEX idx_providers_license_expiry ON providers(license_expiry_date);
FHIR Mapping:
- FHIR Resource:
Practitioner/PractitionerRole - Key mappings:
provider_id→Practitioner.idstaff_number→Practitioner.identifierfirst_name_en/last_name_en→Practitioner.namelicense_number→Practitioner.qualification.identifierspecialty_code→PractitionerRole.specialtyfacility_id→PractitionerRole.organizationdepartment_id→PractitionerRole.location
provider_credentials
Purpose: Tracks provider credentials, certifications, board certifications, and privileges with expiry dates for credentialing compliance.
| Field Name | Data Type | Length | Nullable | Default | Validation | Description |
|---|---|---|---|---|---|---|
| credential_id | BIGINT | — | NO | AUTO_INCREMENT | — | Primary key |
| provider_id | BIGINT | — | NO | — | FK to providers.provider_id | Provider reference |
| credential_type | VARCHAR | 30 | NO | — | IN ('license','board_cert','degree','privilege','cpr_cert','malpractice_ins','other') | Credential category |
| credential_name | VARCHAR | 200 | NO | — | Non-empty | Credential description |
| credential_number | VARCHAR | 100 | YES | NULL | — | Certificate/license number |
| issuing_authority | VARCHAR | 200 | NO | — | — | Issuing organization |
| issue_date | DATE | — | YES | NULL | — | Date issued |
| expiry_date | DATE | — | YES | NULL | >= issue_date | Expiry date |
| status | VARCHAR | 20 | NO | 'active' | IN ('active','expired','revoked','pending_renewal','suspended') | Credential status |
| document_id | BIGINT | — | YES | NULL | FK to patient_documents.document_id | Scanned credential document |
| verified_by | BIGINT | — | YES | NULL | FK to users.user_id | User who verified |
| verified_at | TIMESTAMP | — | YES | NULL | — | Verification timestamp |
| created_at | TIMESTAMP | — | NO | CURRENT_TIMESTAMP | — | Record creation |
| updated_at | TIMESTAMP | — | NO | CURRENT_TIMESTAMP ON UPDATE | — | Last update |
Indexes:
CREATE INDEX idx_credentials_provider ON provider_credentials(provider_id);
CREATE INDEX idx_credentials_type ON provider_credentials(credential_type);
CREATE INDEX idx_credentials_expiry ON provider_credentials(expiry_date) WHERE status = 'active';
users
Purpose: System user accounts for authentication and authorization. Links to providers for clinical users or patients for portal users. Canonical definition — all modules reference via FK.
| Field Name | Data Type | Length | Nullable | Default | Validation | Description |
|---|---|---|---|---|---|---|
| user_id | BIGINT | — | NO | AUTO_INCREMENT | — | Primary key |
| username | VARCHAR | 50 | NO | — | UNIQUE; alphanumeric + underscore | Login username |
| VARCHAR | 200 | NO | — | UNIQUE; valid email | Email address | |
| password_hash | VARCHAR | 255 | NO | — | bcrypt or argon2 hash | Hashed password |
| provider_id | BIGINT | — | YES | NULL | FK to providers.provider_id | Linked provider (clinical users) |
| patient_id | BIGINT | — | YES | NULL | FK to patients.patient_id | Linked patient (portal users) |
| user_type | VARCHAR | 20 | NO | — | IN ('clinical','administrative','portal_patient','portal_provider','system','integration') | User category |
| display_name | VARCHAR | 200 | NO | — | Non-empty | Display name |
| preferred_language | VARCHAR | 5 | YES | 'en' | BCP-47 code | UI language preference |
| is_active | BOOLEAN | — | NO | TRUE | — | Account active flag |
| is_locked | BOOLEAN | — | NO | FALSE | — | Account locked (failed logins) |
| locked_at | TIMESTAMP | — | YES | NULL | — | When account was locked |
| last_login_at | TIMESTAMP | — | YES | NULL | — | Last successful login |
| last_login_ip | VARCHAR | 45 | YES | NULL | — | Last login IP (IPv4/IPv6) |
| failed_login_count | INT | — | NO | 0 | >= 0 | Consecutive failed login attempts |
| password_changed_at | TIMESTAMP | — | YES | NULL | — | Last password change |
| mfa_enabled | BOOLEAN | — | NO | FALSE | — | Multi-factor authentication enabled |
| mfa_secret | VARCHAR | 255 | YES | NULL | Encrypted | MFA secret (TOTP) |
| created_by | BIGINT | — | YES | NULL | FK to users.user_id (self-ref) | Created by (NULL for system) |
| created_at | TIMESTAMP | — | NO | CURRENT_TIMESTAMP | — | Account creation |
| updated_at | TIMESTAMP | — | NO | CURRENT_TIMESTAMP ON UPDATE | — | Last update |
Indexes:
CREATE UNIQUE INDEX idx_users_username ON users(username);
CREATE UNIQUE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_provider ON users(provider_id) WHERE provider_id IS NOT NULL;
CREATE INDEX idx_users_patient ON users(patient_id) WHERE patient_id IS NOT NULL;
CREATE INDEX idx_users_active ON users(is_active) WHERE is_active = TRUE;
CREATE INDEX idx_users_type ON users(user_type);
Constraints:
ALTER TABLE users
ADD CONSTRAINT fk_user_provider FOREIGN KEY (provider_id) REFERENCES providers(provider_id),
ADD CONSTRAINT fk_user_patient FOREIGN KEY (patient_id) REFERENCES patients(patient_id),
ADD CONSTRAINT chk_user_link CHECK (
(user_type = 'portal_patient' AND patient_id IS NOT NULL) OR
(user_type IN ('clinical','portal_provider') AND provider_id IS NOT NULL) OR
(user_type IN ('administrative','system','integration'))
);
roles
Purpose: RBAC role definitions. Roles are assigned to users via user_roles and grant permissions via role_permissions.
| Field Name | Data Type | Length | Nullable | Default | Validation | Description |
|---|---|---|---|---|---|---|
| role_id | BIGINT | — | NO | AUTO_INCREMENT | — | Primary key |
| role_code | VARCHAR | 50 | NO | — | UNIQUE; immutable once created | Machine-readable role identifier |
| role_name_en | VARCHAR | 100 | NO | — | Non-empty | Role display name (English) |
| role_name_ar | VARCHAR | 100 | YES | NULL | — | Role display name (Arabic) |
| description | TEXT | — | YES | NULL | — | Role description and scope |
| role_category | VARCHAR | 30 | NO | — | IN ('clinical','administrative','portal','system','integration') | Role category |
| is_system_role | BOOLEAN | — | NO | FALSE | — | System-managed role (cannot be deleted) |
| is_active | BOOLEAN | — | NO | TRUE | — | Active role flag |
| created_by | BIGINT | — | NO | — | FK to users.user_id | Created by |
| created_at | TIMESTAMP | — | NO | CURRENT_TIMESTAMP | — | Created timestamp |
| updated_at | TIMESTAMP | — | NO | CURRENT_TIMESTAMP ON UPDATE | — | Updated timestamp |
Indexes:
CREATE UNIQUE INDEX idx_roles_code ON roles(role_code);
CREATE INDEX idx_roles_category ON roles(role_category);
CREATE INDEX idx_roles_active ON roles(is_active) WHERE is_active = TRUE;
permissions
Purpose: Granular permission definitions for RBAC. Each permission represents one action on one resource within one module.
| Field Name | Data Type | Length | Nullable | Default | Validation | Description |
|---|---|---|---|---|---|---|
| permission_id | BIGINT | — | NO | AUTO_INCREMENT | — | Primary key |
| permission_code | VARCHAR | 80 | NO | — | UNIQUE; format: module.resource.action | Machine-readable permission ID |
| module | VARCHAR | 30 | NO | — | Module identifier | Module this permission belongs to |
| resource | VARCHAR | 50 | NO | — | — | Resource being acted upon |
| action | VARCHAR | 20 | NO | — | IN ('create','read','update','delete','sign','approve','export','print') | Action type |
| description | TEXT | — | YES | NULL | — | Permission description |
| is_sensitive | BOOLEAN | — | NO | FALSE | — | Requires elevated audit logging |
| created_at | TIMESTAMP | — | NO | CURRENT_TIMESTAMP | — | Created timestamp |
Indexes:
CREATE UNIQUE INDEX idx_permissions_code ON permissions(permission_code);
CREATE INDEX idx_permissions_module ON permissions(module);
CREATE UNIQUE INDEX idx_permissions_unique ON permissions(module, resource, action);
role_permissions
Purpose: Junction table linking roles to permissions.
| Field Name | Data Type | Length | Nullable | Default | Validation | Description |
|---|---|---|---|---|---|---|
| role_id | BIGINT | — | NO | — | FK to roles.role_id | Role reference |
| permission_id | BIGINT | — | NO | — | FK to permissions.permission_id | Permission reference |
| granted_at | TIMESTAMP | — | NO | CURRENT_TIMESTAMP | — | When permission was assigned |
| granted_by | BIGINT | — | NO | — | FK to users.user_id | Who assigned it |
Primary Key: (role_id, permission_id)
Indexes:
CREATE INDEX idx_role_perms_role ON role_permissions(role_id);
CREATE INDEX idx_role_perms_perm ON role_permissions(permission_id);
user_roles
Purpose: Junction table assigning roles to users, scoped to facility. Users can hold different roles at different facilities.
| Field Name | Data Type | Length | Nullable | Default | Validation | Description |
|---|---|---|---|---|---|---|
| user_id | BIGINT | — | NO | — | FK to users.user_id | User reference |
| role_id | BIGINT | — | NO | — | FK to roles.role_id | Role reference |
| facility_id | BIGINT | — | YES | NULL | FK to facilities.facility_id; NULL = all facilities | Facility scope |
| is_active | BOOLEAN | — | NO | TRUE | — | Assignment active flag |
| effective_from | DATE | — | NO | CURRENT_DATE | — | Assignment start date |
| effective_to | DATE | — | YES | NULL | >= effective_from | Assignment end date |
| assigned_by | BIGINT | — | NO | — | FK to users.user_id | Who assigned role |
| assigned_at | TIMESTAMP | — | NO | CURRENT_TIMESTAMP | — | Assignment timestamp |
Primary Key: (user_id, role_id, facility_id) — with COALESCE for NULL facility
Indexes:
CREATE INDEX idx_user_roles_user ON user_roles(user_id);
CREATE INDEX idx_user_roles_role ON user_roles(role_id);
CREATE INDEX idx_user_roles_facility ON user_roles(facility_id);
CREATE INDEX idx_user_roles_active ON user_roles(user_id, is_active) WHERE is_active = TRUE;
facilities
Purpose: Healthcare facility registry. Canonical definition — all modules reference via FK. Supports multi-emirate, multi-facility deployments.
| Field Name | Data Type | Length | Nullable | Default | Validation | Description |
|---|---|---|---|---|---|---|
| facility_id | BIGINT | — | NO | AUTO_INCREMENT | — | Primary key |
| facility_code | VARCHAR | 20 | NO | — | UNIQUE | Short code (e.g., HOSP-DXB-01) |
| facility_name_en | VARCHAR | 200 | NO | — | Non-empty | Facility name (English) |
| facility_name_ar | VARCHAR | 200 | YES | NULL | — | Facility name (Arabic) |
| facility_type | VARCHAR | 30 | NO | — | IN ('hospital','clinic','day_surgery','pharmacy','lab','diagnostic_center','rehabilitation','home_health') | Facility category |
| licensing_authority | VARCHAR | 10 | NO | — | IN ('DOH','DHA','MOH') | Licensing regulator |
| license_number | VARCHAR | 50 | NO | — | Non-empty | Facility license number |
| license_expiry_date | DATE | — | NO | — | — | License expiry |
| emirate_code | VARCHAR | 3 | NO | — | IN ('DXB','AUH','SHJ','AJM','RAK','UAQ','FUJ') | Emirate location |
| city | VARCHAR | 100 | YES | NULL | — | City |
| address_en | VARCHAR | 300 | YES | NULL | — | Physical address (English) |
| address_ar | VARCHAR | 300 | YES | NULL | — | Physical address (Arabic) |
| phone | VARCHAR | 20 | YES | NULL | E.164 format | Main phone number |
| nabidh_facility_id | VARCHAR | 30 | YES | NULL | Required if emirate_code = 'DXB' | NABIDH-assigned facility ID |
| malaffi_facility_id | VARCHAR | 30 | YES | NULL | Required if emirate_code = 'AUH' | Malaffi-assigned facility ID |
| mrn_prefix | VARCHAR | 10 | YES | NULL | UNIQUE if set | Facility-specific MRN prefix |
| timezone | VARCHAR | 50 | NO | 'Asia/Dubai' | Valid IANA timezone | Facility timezone |
| is_active | BOOLEAN | — | NO | TRUE | — | Active facility flag |
| created_at | TIMESTAMP | — | NO | CURRENT_TIMESTAMP | — | Created timestamp |
| updated_at | TIMESTAMP | — | NO | CURRENT_TIMESTAMP ON UPDATE | — | Updated timestamp |
Indexes:
CREATE UNIQUE INDEX idx_facilities_code ON facilities(facility_code);
CREATE INDEX idx_facilities_emirate ON facilities(emirate_code);
CREATE INDEX idx_facilities_type ON facilities(facility_type);
CREATE INDEX idx_facilities_authority ON facilities(licensing_authority);
CREATE INDEX idx_facilities_active ON facilities(is_active) WHERE is_active = TRUE;
FHIR Mapping:
- FHIR Resource:
Organization/Location - Key mappings:
facility_id→Organization.idfacility_code→Organization.identifierfacility_name_en→Organization.namefacility_type→Organization.typelicense_number→Organization.identifier[type=license]
departments
Purpose: Departments within facilities. Used for access scoping, order routing, and reporting.
| Field Name | Data Type | Length | Nullable | Default | Validation | Description |
|---|---|---|---|---|---|---|
| department_id | BIGINT | — | NO | AUTO_INCREMENT | — | Primary key |
| facility_id | BIGINT | — | NO | — | FK to facilities.facility_id | Parent facility |
| department_code | VARCHAR | 20 | NO | — | UNIQUE within facility | Department short code |
| department_name_en | VARCHAR | 200 | NO | — | Non-empty | Department name (English) |
| department_name_ar | VARCHAR | 200 | YES | NULL | — | Department name (Arabic) |
| department_type | VARCHAR | 30 | NO | — | IN ('clinical','administrative','support','ancillary') | Category |
| parent_department_id | BIGINT | — | YES | NULL | FK to departments.department_id (self-ref) | Parent department (for hierarchy) |
| cost_center_code | VARCHAR | 20 | YES | NULL | — | Financial cost center |
| phone_extension | VARCHAR | 10 | YES | NULL | — | Internal phone extension |
| is_active | BOOLEAN | — | NO | TRUE | — | Active flag |
| created_at | TIMESTAMP | — | NO | CURRENT_TIMESTAMP | — | Created timestamp |
| updated_at | TIMESTAMP | — | NO | CURRENT_TIMESTAMP ON UPDATE | — | Updated timestamp |
Indexes:
CREATE UNIQUE INDEX idx_depts_code ON departments(facility_id, department_code);
CREATE INDEX idx_depts_facility ON departments(facility_id);
CREATE INDEX idx_depts_parent ON departments(parent_department_id);
CREATE INDEX idx_depts_type ON departments(department_type);
locations
Purpose: Physical locations within departments — rooms, beds, stations, bays. Used by Scheduling for bed management and by all modules for patient location tracking.
| Field Name | Data Type | Length | Nullable | Default | Validation | Description |
|---|---|---|---|---|---|---|
| location_id | BIGINT | — | NO | AUTO_INCREMENT | — | Primary key |
| department_id | BIGINT | — | NO | — | FK to departments.department_id | Parent department |
| facility_id | BIGINT | — | NO | — | FK to facilities.facility_id | Parent facility (denormalized for queries) |
| location_code | VARCHAR | 20 | NO | — | UNIQUE within facility | Location short code |
| location_name | VARCHAR | 100 | NO | — | Non-empty | Location display name |
| location_type | VARCHAR | 30 | NO | — | IN ('room','bed','station','bay','or_suite','exam_room','waiting_area','nursing_station','pharmacy','lab','other') | Location category |
| floor | VARCHAR | 10 | YES | NULL | — | Floor/level |
| wing | VARCHAR | 20 | YES | NULL | — | Building wing |
| bed_type | VARCHAR | 20 | YES | NULL | IN ('standard','icu','nicu','isolation','bariatric','pediatric') when location_type = 'bed' | Bed category |
| is_active | BOOLEAN | — | NO | TRUE | — | Active flag |
| is_occupied | BOOLEAN | — | NO | FALSE | — | Current occupancy status |
| created_at | TIMESTAMP | — | NO | CURRENT_TIMESTAMP | — | Created timestamp |
| updated_at | TIMESTAMP | — | NO | CURRENT_TIMESTAMP ON UPDATE | — | Updated timestamp |
Indexes:
CREATE UNIQUE INDEX idx_locations_code ON locations(facility_id, location_code);
CREATE INDEX idx_locations_dept ON locations(department_id);
CREATE INDEX idx_locations_facility ON locations(facility_id);
CREATE INDEX idx_locations_type ON locations(location_type);
CREATE INDEX idx_locations_available ON locations(location_type, is_active, is_occupied) WHERE location_type = 'bed' AND is_active = TRUE AND is_occupied = FALSE;
audit_log
Purpose: Immutable audit trail for all user actions across the HIS. Required by UAE PDPL, NESA/ADHICS, DOH, and DHA regulations. Supports break-the-glass (BTG) tracking, data access logging, and compliance reporting. This table is append-only — no UPDATE or DELETE operations permitted.
| Field Name | Data Type | Length | Nullable | Default | Validation | Description |
|---|---|---|---|---|---|---|
| audit_id | BIGINT | — | NO | AUTO_INCREMENT | — | Primary key |
| user_id | BIGINT | — | NO | — | FK to users.user_id | Acting user |
| session_id | VARCHAR | 64 | YES | NULL | — | User session identifier |
| action | VARCHAR | 30 | NO | — | FK to ref_audit_actions.action_code | Action performed |
| resource_type | VARCHAR | 50 | NO | — | — | Resource/entity type (e.g., 'patients', 'clinical_notes', 'HL7_ADT') |
| resource_id | BIGINT | — | YES | NULL | — | Specific resource ID |
| patient_id | BIGINT | — | YES | NULL | FK to patients.patient_id | Patient context (if applicable) |
| encounter_id | BIGINT | — | YES | NULL | FK to encounters.encounter_id | Encounter context (if applicable) |
| module | VARCHAR | 30 | YES | NULL | — | Module where action occurred |
| event_timestamp | TIMESTAMP | — | NO | CURRENT_TIMESTAMP | — | Event timestamp (UTC) |
| ip_address | VARCHAR | 45 | YES | NULL | — | Client IP address (IPv4/IPv6) |
| user_agent | VARCHAR | 500 | YES | NULL | — | Client user agent string |
| old_values | JSON | — | YES | NULL | — | Previous field values (for updates) |
| new_values | JSON | — | YES | NULL | — | New field values (for creates/updates) |
| is_btg | BOOLEAN | — | NO | FALSE | — | Break-the-glass access |
| btg_reason_code | VARCHAR | 30 | YES | NULL | FK to ref_btg_reasons.btg_reason_code; required if is_btg = TRUE | BTG reason |
| btg_justification | TEXT | — | YES | NULL | — | Free-text BTG justification |
| is_phi_access | BOOLEAN | — | NO | FALSE | — | Accessed protected health information |
| outcome | VARCHAR | 20 | NO | 'success' | IN ('success','failure','denied','error') | Action outcome |
| error_message | TEXT | — | YES | NULL | — | Error details if outcome != 'success' |
Indexes:
CREATE INDEX idx_audit_user ON audit_log(user_id);
CREATE INDEX idx_audit_patient ON audit_log(patient_id);
CREATE INDEX idx_audit_resource ON audit_log(resource_type, resource_id);
CREATE INDEX idx_audit_timestamp ON audit_log(event_timestamp);
CREATE INDEX idx_audit_action ON audit_log(action);
CREATE INDEX idx_audit_btg ON audit_log(is_btg, event_timestamp) WHERE is_btg = TRUE;
CREATE INDEX idx_audit_module ON audit_log(module, event_timestamp);
CREATE INDEX idx_audit_outcome ON audit_log(outcome) WHERE outcome != 'success';
Partitioning:
-- Partition by month for query performance and archival
CREATE TABLE audit_log (
...
) PARTITION BY RANGE (EXTRACT(YEAR_MONTH FROM event_timestamp));
-- Example partitions
ALTER TABLE audit_log ADD PARTITION (
PARTITION p202601 VALUES LESS THAN (202602),
PARTITION p202602 VALUES LESS THAN (202603)
);
Regulatory Note: Audit log records must be retained for a minimum of 10 years per DOH/DHA requirements. Records must be immutable — implement via database triggers or application-level enforcement that prevents UPDATE/DELETE on this table. Consider write-once archival storage for partitions older than 1 year.
integration_message_log
Purpose: Tracks all inbound and outbound integration messages (HL7 v2.x, FHIR API calls, eligibility checks). Supports retry logic, dead-letter queue, and integration monitoring.
| Field Name | Data Type | Length | Nullable | Default | Validation | Description |
|---|---|---|---|---|---|---|
| message_id | BIGINT | — | NO | AUTO_INCREMENT | — | Primary key |
| message_control_id | VARCHAR | 50 | YES | NULL | — | HL7 MSH-10 message control ID |
| direction | VARCHAR | 10 | NO | — | IN ('outbound','inbound') | Message direction |
| message_type | VARCHAR | 30 | NO | — | e.g., 'ADT_A04', 'FHIR_Patient', 'ELIGIBILITY' | Message/event type |
| target_system | VARCHAR | 50 | NO | — | e.g., 'NABIDH', 'MALAFFI', 'SCHEDULING', 'BILLING' | Target/source system |
| integration_endpoint | VARCHAR | 200 | YES | NULL | — | Endpoint URL or connection ID |
| patient_id | BIGINT | — | YES | NULL | FK to patients.patient_id | Patient context |
| status | VARCHAR | 20 | NO | 'queued' | IN ('queued','sent','pending','acknowledged','error','failed','rejected') | Message status |
| payload | TEXT | — | YES | NULL | — | Message payload (truncated for large messages) |
| response_payload | TEXT | — | YES | NULL | — | Response/ACK payload |
| ack_code | VARCHAR | 5 | YES | NULL | IN ('AA','AE','AR') for HL7; HTTP status for FHIR | Acknowledgment code |
| retry_count | INT | — | NO | 0 | >= 0 | Number of retry attempts |
| max_retries | INT | — | NO | 10 | > 0 | Maximum retry attempts |
| next_retry_at | TIMESTAMP | — | YES | NULL | — | Next scheduled retry |
| error_message | TEXT | — | YES | NULL | — | Error details |
| created_at | TIMESTAMP | — | NO | CURRENT_TIMESTAMP | — | Message creation |
| sent_at | TIMESTAMP | — | YES | NULL | — | When message was sent |
| completed_at | TIMESTAMP | — | YES | NULL | — | When final status reached |
Indexes:
CREATE INDEX idx_intmsg_patient ON integration_message_log(patient_id);
CREATE INDEX idx_intmsg_status ON integration_message_log(status);
CREATE INDEX idx_intmsg_type ON integration_message_log(message_type);
CREATE INDEX idx_intmsg_target ON integration_message_log(target_system);
CREATE INDEX idx_intmsg_retry ON integration_message_log(next_retry_at) WHERE status IN ('queued','error') AND retry_count < max_retries;
CREATE INDEX idx_intmsg_deadletter ON integration_message_log(status, created_at) WHERE status IN ('failed','rejected');
CREATE INDEX idx_intmsg_control ON integration_message_log(message_control_id);
Reference Tables
The following reference/master data tables support the transactional tables above. Full definitions are in 06-master-data.md; summary provided here.
| Reference Table | Purpose | Key Fields |
|---|---|---|
ref_nationalities |
ISO nationalities with Arabic names | nationality_code (PK), display_name_en, display_name_ar |
ref_emirates_cities |
UAE emirates and cities | emirate_code, city_code (composite PK) |
ref_document_types |
Document category codes | document_type_code (PK), category, default_retention_years |
ref_note_templates |
Clinical note templates | template_id (PK), note_type, specialty_code, content_structure_json |
ref_consent_templates |
Consent form templates | template_id (PK), consent_type_code, version, body_text |
ref_consent_types |
Consent type definitions | consent_type_code (PK), description, is_mandatory |
ref_payers |
Insurance payer reference (local copy from policy-contract-mgmt) | payer_id FK, payer_code, dha_payer_code, doh_payer_code |
ref_provider_specialties |
Medical specialty codes | specialty_code (PK), specialty_name_en, doh_code, dha_code |
ref_allergens |
Common allergen master list | allergen_id (PK), allergen_code, allergen_system, description |
ref_snomed_terms |
SNOMED CT term subset | snomed_code (PK), preferred_term, semantic_tag |
ref_audit_actions |
Standardized audit action codes | action_code (PK), action_description, module |
ref_btg_reasons |
Break-the-glass reason codes | btg_reason_code (PK), description, requires_free_text |
Sample SQL DDL
-- ============================================================
-- EHR & Patient Management Module — Core Tables DDL
-- Target: PostgreSQL 15+ (syntax adaptable to MySQL 8+)
-- ============================================================
-- -------------------------------------------------------
-- 1. patients (Shared Entity — canonical definition)
-- -------------------------------------------------------
CREATE TABLE patients (
patient_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
mrn VARCHAR(20) NOT NULL UNIQUE,
first_name_en VARCHAR(100) NOT NULL,
middle_name_en VARCHAR(100),
last_name_en VARCHAR(100) NOT NULL,
first_name_ar VARCHAR(100),
middle_name_ar VARCHAR(100),
last_name_ar VARCHAR(100),
date_of_birth DATE NOT NULL,
gender VARCHAR(10) NOT NULL,
nationality_code VARCHAR(3),
is_vip BOOLEAN NOT NULL DEFAULT FALSE,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
is_deceased BOOLEAN NOT NULL DEFAULT FALSE,
deceased_datetime TIMESTAMP,
merged_into_patient_id BIGINT,
blood_group VARCHAR(5),
preferred_language VARCHAR(5) DEFAULT 'en',
photo_url VARCHAR(500),
created_by BIGINT NOT NULL,
updated_by BIGINT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_patient_merged FOREIGN KEY (merged_into_patient_id) REFERENCES patients(patient_id),
CONSTRAINT chk_gender CHECK (gender IN ('male', 'female', 'unknown')),
CONSTRAINT chk_dob CHECK (date_of_birth <= CURRENT_DATE),
CONSTRAINT chk_deceased CHECK (is_deceased = FALSE OR deceased_datetime IS NOT NULL),
CONSTRAINT chk_blood_group CHECK (blood_group IS NULL OR blood_group IN ('A+','A-','B+','B-','AB+','AB-','O+','O-'))
);
-- -------------------------------------------------------
-- 2. patient_demographics (versioned — SCD Type 2)
-- -------------------------------------------------------
CREATE TABLE patient_demographics (
demographic_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
patient_id BIGINT NOT NULL,
is_current BOOLEAN NOT NULL DEFAULT TRUE,
effective_from TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
effective_to TIMESTAMP,
address_line1_en VARCHAR(200),
address_line2_en VARCHAR(200),
address_line1_ar VARCHAR(200),
po_box VARCHAR(20),
emirate_code VARCHAR(3),
city VARCHAR(100),
country_code VARCHAR(3) DEFAULT 'ARE',
mobile_phone VARCHAR(20),
home_phone VARCHAR(20),
work_phone VARCHAR(20),
email VARCHAR(200),
marital_status VARCHAR(20),
occupation VARCHAR(100),
employer_name VARCHAR(200),
religion VARCHAR(50),
created_by BIGINT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_demo_patient FOREIGN KEY (patient_id) REFERENCES patients(patient_id),
CONSTRAINT chk_marital CHECK (marital_status IS NULL OR marital_status IN ('single','married','divorced','widowed','separated','unknown'))
);
CREATE UNIQUE INDEX idx_patient_demo_current ON patient_demographics(patient_id) WHERE is_current = TRUE;
-- -------------------------------------------------------
-- 3. patient_identifiers
-- -------------------------------------------------------
CREATE TABLE patient_identifiers (
identifier_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
patient_id BIGINT NOT NULL,
identifier_type VARCHAR(30) NOT NULL,
identifier_value VARCHAR(100) NOT NULL,
identifier_system VARCHAR(200),
is_primary BOOLEAN NOT NULL DEFAULT TRUE,
verification_status VARCHAR(20) NOT NULL DEFAULT 'unverified',
issue_date DATE,
expiry_date DATE,
issuing_authority VARCHAR(100),
created_by BIGINT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_ident_patient FOREIGN KEY (patient_id) REFERENCES patients(patient_id),
CONSTRAINT chk_ident_type CHECK (identifier_type IN ('MRN','EMIRATES_ID','PASSPORT','VISA','INSURANCE_MEMBER_ID','OLD_MRN','NATIONAL_ID','OTHER')),
CONSTRAINT chk_ident_status CHECK (verification_status IN ('verified','unverified','expired','invalid')),
CONSTRAINT chk_eid_format CHECK (identifier_type != 'EMIRATES_ID' OR identifier_value ~ '^784-[0-9]{4}-[0-9]{7}-[0-9]$')
);
-- -------------------------------------------------------
-- 4. facilities (Shared Entity — canonical definition)
-- -------------------------------------------------------
CREATE TABLE facilities (
facility_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
facility_code VARCHAR(20) NOT NULL UNIQUE,
facility_name_en VARCHAR(200) NOT NULL,
facility_name_ar VARCHAR(200),
facility_type VARCHAR(30) NOT NULL,
licensing_authority VARCHAR(10) NOT NULL,
license_number VARCHAR(50) NOT NULL,
license_expiry_date DATE NOT NULL,
emirate_code VARCHAR(3) NOT NULL,
city VARCHAR(100),
address_en VARCHAR(300),
address_ar VARCHAR(300),
phone VARCHAR(20),
nabidh_facility_id VARCHAR(30),
malaffi_facility_id VARCHAR(30),
mrn_prefix VARCHAR(10) UNIQUE,
timezone VARCHAR(50) NOT NULL DEFAULT 'Asia/Dubai',
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT chk_facility_type CHECK (facility_type IN ('hospital','clinic','day_surgery','pharmacy','lab','diagnostic_center','rehabilitation','home_health')),
CONSTRAINT chk_licensing_auth CHECK (licensing_authority IN ('DOH','DHA','MOH')),
CONSTRAINT chk_emirate CHECK (emirate_code IN ('DXB','AUH','SHJ','AJM','RAK','UAQ','FUJ'))
);
-- -------------------------------------------------------
-- 5. departments
-- -------------------------------------------------------
CREATE TABLE departments (
department_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
facility_id BIGINT NOT NULL,
department_code VARCHAR(20) NOT NULL,
department_name_en VARCHAR(200) NOT NULL,
department_name_ar VARCHAR(200),
department_type VARCHAR(30) NOT NULL,
parent_department_id BIGINT,
cost_center_code VARCHAR(20),
phone_extension VARCHAR(10),
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_dept_facility FOREIGN KEY (facility_id) REFERENCES facilities(facility_id),
CONSTRAINT fk_dept_parent FOREIGN KEY (parent_department_id) REFERENCES departments(department_id),
CONSTRAINT chk_dept_type CHECK (department_type IN ('clinical','administrative','support','ancillary')),
CONSTRAINT uq_dept_code UNIQUE (facility_id, department_code)
);
-- -------------------------------------------------------
-- 6. locations
-- -------------------------------------------------------
CREATE TABLE locations (
location_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
department_id BIGINT NOT NULL,
facility_id BIGINT NOT NULL,
location_code VARCHAR(20) NOT NULL,
location_name VARCHAR(100) NOT NULL,
location_type VARCHAR(30) NOT NULL,
floor VARCHAR(10),
wing VARCHAR(20),
bed_type VARCHAR(20),
is_active BOOLEAN NOT NULL DEFAULT TRUE,
is_occupied BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_loc_dept FOREIGN KEY (department_id) REFERENCES departments(department_id),
CONSTRAINT fk_loc_facility FOREIGN KEY (facility_id) REFERENCES facilities(facility_id),
CONSTRAINT chk_loc_type CHECK (location_type IN ('room','bed','station','bay','or_suite','exam_room','waiting_area','nursing_station','pharmacy','lab','other')),
CONSTRAINT uq_loc_code UNIQUE (facility_id, location_code)
);
-- -------------------------------------------------------
-- 7. providers (Shared Entity — canonical definition)
-- -------------------------------------------------------
CREATE TABLE providers (
provider_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
staff_number VARCHAR(20) NOT NULL UNIQUE,
first_name_en VARCHAR(100) NOT NULL,
middle_name_en VARCHAR(100),
last_name_en VARCHAR(100) NOT NULL,
first_name_ar VARCHAR(100),
last_name_ar VARCHAR(100),
provider_type VARCHAR(30) NOT NULL,
specialty_code VARCHAR(20),
subspecialty_code VARCHAR(20),
license_number VARCHAR(50) NOT NULL,
licensing_authority VARCHAR(10) NOT NULL,
license_expiry_date DATE NOT NULL,
national_id VARCHAR(20),
email VARCHAR(200),
mobile_phone VARCHAR(20),
department_id BIGINT,
facility_id BIGINT NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
doh_provider_id VARCHAR(30),
dha_provider_id VARCHAR(30),
can_prescribe BOOLEAN NOT NULL DEFAULT FALSE,
can_prescribe_controlled BOOLEAN NOT NULL DEFAULT FALSE,
digital_signature_cert VARCHAR(500),
created_by BIGINT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_prov_dept FOREIGN KEY (department_id) REFERENCES departments(department_id),
CONSTRAINT fk_prov_facility FOREIGN KEY (facility_id) REFERENCES facilities(facility_id),
CONSTRAINT chk_prov_type CHECK (provider_type IN ('physician','nurse','pharmacist','technician','therapist','admin','other')),
CONSTRAINT chk_prov_auth CHECK (licensing_authority IN ('DOH','DHA','MOH'))
);
-- -------------------------------------------------------
-- 8. users (Shared Entity — canonical definition)
-- -------------------------------------------------------
CREATE TABLE users (
user_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
email VARCHAR(200) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
provider_id BIGINT,
patient_id BIGINT,
user_type VARCHAR(20) NOT NULL,
display_name VARCHAR(200) NOT NULL,
preferred_language VARCHAR(5) DEFAULT 'en',
is_active BOOLEAN NOT NULL DEFAULT TRUE,
is_locked BOOLEAN NOT NULL DEFAULT FALSE,
locked_at TIMESTAMP,
last_login_at TIMESTAMP,
last_login_ip VARCHAR(45),
failed_login_count INT NOT NULL DEFAULT 0,
password_changed_at TIMESTAMP,
mfa_enabled BOOLEAN NOT NULL DEFAULT FALSE,
mfa_secret VARCHAR(255),
created_by BIGINT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_user_provider FOREIGN KEY (provider_id) REFERENCES providers(provider_id),
CONSTRAINT fk_user_patient FOREIGN KEY (patient_id) REFERENCES patients(patient_id),
CONSTRAINT chk_user_type CHECK (user_type IN ('clinical','administrative','portal_patient','portal_provider','system','integration'))
);
-- -------------------------------------------------------
-- 9. roles
-- -------------------------------------------------------
CREATE TABLE roles (
role_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
role_code VARCHAR(50) NOT NULL UNIQUE,
role_name_en VARCHAR(100) NOT NULL,
role_name_ar VARCHAR(100),
description TEXT,
role_category VARCHAR(30) NOT NULL,
is_system_role BOOLEAN NOT NULL DEFAULT FALSE,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_by BIGINT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT chk_role_cat CHECK (role_category IN ('clinical','administrative','portal','system','integration'))
);
-- -------------------------------------------------------
-- 10. permissions
-- -------------------------------------------------------
CREATE TABLE permissions (
permission_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
permission_code VARCHAR(80) NOT NULL UNIQUE,
module VARCHAR(30) NOT NULL,
resource VARCHAR(50) NOT NULL,
action VARCHAR(20) NOT NULL,
description TEXT,
is_sensitive BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT chk_perm_action CHECK (action IN ('create','read','update','delete','sign','approve','export','print')),
CONSTRAINT uq_perm UNIQUE (module, resource, action)
);
-- -------------------------------------------------------
-- 11. role_permissions (junction)
-- -------------------------------------------------------
CREATE TABLE role_permissions (
role_id BIGINT NOT NULL,
permission_id BIGINT NOT NULL,
granted_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
granted_by BIGINT NOT NULL,
PRIMARY KEY (role_id, permission_id),
CONSTRAINT fk_rp_role FOREIGN KEY (role_id) REFERENCES roles(role_id),
CONSTRAINT fk_rp_perm FOREIGN KEY (permission_id) REFERENCES permissions(permission_id)
);
-- -------------------------------------------------------
-- 12. user_roles (junction, facility-scoped)
-- -------------------------------------------------------
CREATE TABLE user_roles (
user_id BIGINT NOT NULL,
role_id BIGINT NOT NULL,
facility_id BIGINT,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
effective_from DATE NOT NULL DEFAULT CURRENT_DATE,
effective_to DATE,
assigned_by BIGINT NOT NULL,
assigned_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_ur_user FOREIGN KEY (user_id) REFERENCES users(user_id),
CONSTRAINT fk_ur_role FOREIGN KEY (role_id) REFERENCES roles(role_id),
CONSTRAINT fk_ur_facility FOREIGN KEY (facility_id) REFERENCES facilities(facility_id),
CONSTRAINT chk_ur_dates CHECK (effective_to IS NULL OR effective_to >= effective_from)
);
-- -------------------------------------------------------
-- 13. audit_log (append-only, partitioned)
-- -------------------------------------------------------
CREATE TABLE audit_log (
audit_id BIGINT GENERATED ALWAYS AS IDENTITY,
user_id BIGINT NOT NULL,
session_id VARCHAR(64),
action VARCHAR(30) NOT NULL,
resource_type VARCHAR(50) NOT NULL,
resource_id BIGINT,
patient_id BIGINT,
encounter_id BIGINT,
module VARCHAR(30),
event_timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
ip_address VARCHAR(45),
user_agent VARCHAR(500),
old_values JSONB,
new_values JSONB,
is_btg BOOLEAN NOT NULL DEFAULT FALSE,
btg_reason_code VARCHAR(30),
btg_justification TEXT,
is_phi_access BOOLEAN NOT NULL DEFAULT FALSE,
outcome VARCHAR(20) NOT NULL DEFAULT 'success',
error_message TEXT,
PRIMARY KEY (audit_id, event_timestamp),
CONSTRAINT chk_outcome CHECK (outcome IN ('success','failure','denied','error')),
CONSTRAINT chk_btg CHECK (is_btg = FALSE OR btg_reason_code IS NOT NULL)
) PARTITION BY RANGE (event_timestamp);
-- Create initial partitions
CREATE TABLE audit_log_2026_01 PARTITION OF audit_log
FOR VALUES FROM ('2026-01-01') TO ('2026-02-01');
CREATE TABLE audit_log_2026_02 PARTITION OF audit_log
FOR VALUES FROM ('2026-02-01') TO ('2026-03-01');
CREATE TABLE audit_log_2026_03 PARTITION OF audit_log
FOR VALUES FROM ('2026-03-01') TO ('2026-04-01');
-- Prevent UPDATE/DELETE on audit_log
CREATE OR REPLACE FUNCTION prevent_audit_modification()
RETURNS TRIGGER AS $$
BEGIN
RAISE EXCEPTION 'Audit log records are immutable. UPDATE and DELETE operations are not permitted.';
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_audit_no_update
BEFORE UPDATE OR DELETE ON audit_log
FOR EACH ROW EXECUTE FUNCTION prevent_audit_modification();
-- -------------------------------------------------------
-- 14. patient_allergies
-- -------------------------------------------------------
CREATE TABLE patient_allergies (
allergy_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
patient_id BIGINT NOT NULL,
encounter_id BIGINT,
allergen_type VARCHAR(20) NOT NULL,
allergen_description VARCHAR(300) NOT NULL,
allergen_code VARCHAR(20),
allergen_system VARCHAR(50),
reaction_description VARCHAR(500),
reaction_code VARCHAR(20),
severity VARCHAR(20) NOT NULL DEFAULT 'moderate',
criticality VARCHAR(10),
status VARCHAR(20) NOT NULL DEFAULT 'active',
onset_date DATE,
verification_status VARCHAR(20) NOT NULL DEFAULT 'unconfirmed',
entered_by BIGINT NOT NULL,
verified_by BIGINT,
verified_at TIMESTAMP,
notes TEXT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_allergy_patient FOREIGN KEY (patient_id) REFERENCES patients(patient_id),
CONSTRAINT fk_allergy_entered FOREIGN KEY (entered_by) REFERENCES users(user_id),
CONSTRAINT chk_allergen_type CHECK (allergen_type IN ('medication','food','environmental','biologic','other')),
CONSTRAINT chk_allergy_severity CHECK (severity IN ('mild','moderate','severe','life-threatening')),
CONSTRAINT chk_allergy_status CHECK (status IN ('active','inactive','resolved','entered-in-error','nka','nkda')),
CONSTRAINT chk_allergy_verif CHECK (verification_status IN ('unconfirmed','confirmed','refuted','entered-in-error'))
);
-- -------------------------------------------------------
-- 15. patient_problems
-- -------------------------------------------------------
CREATE TABLE patient_problems (
problem_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
patient_id BIGINT NOT NULL,
encounter_id BIGINT,
problem_description VARCHAR(500) NOT NULL,
icd10_code VARCHAR(10),
icd10_description VARCHAR(300),
snomed_code VARCHAR(20),
snomed_description VARCHAR(300),
problem_type VARCHAR(20) NOT NULL DEFAULT 'problem',
status VARCHAR(20) NOT NULL DEFAULT 'active',
severity VARCHAR(20),
onset_date DATE,
resolved_date DATE,
is_chronic BOOLEAN NOT NULL DEFAULT FALSE,
is_principal BOOLEAN NOT NULL DEFAULT FALSE,
entered_by BIGINT NOT NULL,
verified_by BIGINT,
notes TEXT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_problem_patient FOREIGN KEY (patient_id) REFERENCES patients(patient_id),
CONSTRAINT fk_problem_entered FOREIGN KEY (entered_by) REFERENCES users(user_id),
CONSTRAINT chk_problem_type CHECK (problem_type IN ('problem','diagnosis','symptom','finding','complaint')),
CONSTRAINT chk_problem_status CHECK (status IN ('active','resolved','inactive','recurrence','remission')),
CONSTRAINT chk_resolved_date CHECK (resolved_date IS NULL OR resolved_date >= onset_date)
);
-- -------------------------------------------------------
-- 16. patient_consents
-- -------------------------------------------------------
CREATE TABLE patient_consents (
consent_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
patient_id BIGINT NOT NULL,
encounter_id BIGINT,
consent_type_code VARCHAR(50) NOT NULL,
consent_template_id BIGINT,
status VARCHAR(20) NOT NULL,
processing_legal_basis VARCHAR(30) NOT NULL,
granted_datetime TIMESTAMP,
withdrawn_datetime TIMESTAMP,
expiry_datetime TIMESTAMP,
signature_method VARCHAR(30),
signature_data TEXT,
document_id BIGINT,
witnessed_by BIGINT,
collected_by BIGINT NOT NULL,
notes TEXT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_consent_patient FOREIGN KEY (patient_id) REFERENCES patients(patient_id),
CONSTRAINT chk_consent_status CHECK (status IN ('granted','declined','withdrawn','expired')),
CONSTRAINT chk_consent_basis CHECK (processing_legal_basis IN ('explicit_consent','treatment_exemption','public_health','legal_obligation','vital_interest','contractual')),
CONSTRAINT chk_granted CHECK (status != 'granted' OR granted_datetime IS NOT NULL),
CONSTRAINT chk_withdrawn CHECK (status != 'withdrawn' OR withdrawn_datetime IS NOT NULL)
);
-- -------------------------------------------------------
-- 17. patient_documents
-- -------------------------------------------------------
CREATE TABLE patient_documents (
document_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
patient_id BIGINT NOT NULL,
encounter_id BIGINT,
document_type_code VARCHAR(50) NOT NULL,
document_title VARCHAR(300) NOT NULL,
mime_type VARCHAR(100) NOT NULL,
file_size_bytes BIGINT,
storage_path VARCHAR(500) NOT NULL,
storage_system VARCHAR(30) NOT NULL DEFAULT 'dms',
status VARCHAR(20) NOT NULL DEFAULT 'active',
uploaded_by BIGINT NOT NULL,
upload_datetime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
retention_expiry DATE,
checksum_sha256 VARCHAR(64),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_doc_patient FOREIGN KEY (patient_id) REFERENCES patients(patient_id),
CONSTRAINT fk_doc_uploaded FOREIGN KEY (uploaded_by) REFERENCES users(user_id),
CONSTRAINT chk_doc_status CHECK (status IN ('active','archived','deleted','pending_scan'))
);
-- -------------------------------------------------------
-- 18. clinical_notes
-- -------------------------------------------------------
CREATE TABLE clinical_notes (
note_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
patient_id BIGINT NOT NULL,
encounter_id BIGINT NOT NULL,
note_type VARCHAR(30) NOT NULL,
template_id BIGINT,
note_title VARCHAR(300) NOT NULL,
note_body TEXT NOT NULL,
structured_data JSONB,
status VARCHAR(20) NOT NULL DEFAULT 'draft',
author_provider_id BIGINT NOT NULL,
authored_datetime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
signed_by BIGINT,
signed_datetime TIMESTAMP,
cosigner_provider_id BIGINT,
cosigned_datetime TIMESTAMP,
parent_note_id BIGINT,
specialty_code VARCHAR(20),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_note_patient FOREIGN KEY (patient_id) REFERENCES patients(patient_id),
CONSTRAINT fk_note_author FOREIGN KEY (author_provider_id) REFERENCES providers(provider_id),
CONSTRAINT fk_note_signer FOREIGN KEY (signed_by) REFERENCES providers(provider_id),
CONSTRAINT fk_note_cosigner FOREIGN KEY (cosigner_provider_id) REFERENCES providers(provider_id),
CONSTRAINT fk_note_parent FOREIGN KEY (parent_note_id) REFERENCES clinical_notes(note_id),
CONSTRAINT chk_note_type CHECK (note_type IN ('H_AND_P','PROGRESS','DISCHARGE','CONSULT','NURSING_ASSESSMENT','PROCEDURE','OPERATIVE','ED','ADDENDUM')),
CONSTRAINT chk_note_status CHECK (status IN ('draft','pending_cosign','signed','amended','entered_in_error'))
);
-- -------------------------------------------------------
-- 19. duplicate_suspects
-- -------------------------------------------------------
CREATE TABLE duplicate_suspects (
suspect_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
patient_id_1 BIGINT NOT NULL,
patient_id_2 BIGINT NOT NULL,
match_score DECIMAL(5,2) NOT NULL,
match_method VARCHAR(30) NOT NULL,
matching_fields JSONB,
status VARCHAR(20) NOT NULL DEFAULT 'new',
resolution VARCHAR(20),
primary_patient_id BIGINT,
flagged_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
flagged_by BIGINT,
reviewed_by BIGINT,
reviewed_at TIMESTAMP,
approved_by BIGINT,
approved_at TIMESTAMP,
notes TEXT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_dupe_p1 FOREIGN KEY (patient_id_1) REFERENCES patients(patient_id),
CONSTRAINT fk_dupe_p2 FOREIGN KEY (patient_id_2) REFERENCES patients(patient_id),
CONSTRAINT chk_dupe_diff CHECK (patient_id_1 != patient_id_2),
CONSTRAINT chk_dupe_status CHECK (status IN ('new','in_review','pending_approval','resolved','rejected','auto_resolved')),
CONSTRAINT chk_dupe_method CHECK (match_method IN ('deterministic','probabilistic','manual','registration_flag')),
CONSTRAINT chk_dupe_score CHECK (match_score BETWEEN 0.00 AND 100.00)
);
-- -------------------------------------------------------
-- 20. provider_credentials
-- -------------------------------------------------------
CREATE TABLE provider_credentials (
credential_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
provider_id BIGINT NOT NULL,
credential_type VARCHAR(30) NOT NULL,
credential_name VARCHAR(200) NOT NULL,
credential_number VARCHAR(100),
issuing_authority VARCHAR(200) NOT NULL,
issue_date DATE,
expiry_date DATE,
status VARCHAR(20) NOT NULL DEFAULT 'active',
document_id BIGINT,
verified_by BIGINT,
verified_at TIMESTAMP,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_cred_provider FOREIGN KEY (provider_id) REFERENCES providers(provider_id),
CONSTRAINT chk_cred_type CHECK (credential_type IN ('license','board_cert','degree','privilege','cpr_cert','malpractice_ins','other')),
CONSTRAINT chk_cred_status CHECK (status IN ('active','expired','revoked','pending_renewal','suspended')),
CONSTRAINT chk_cred_dates CHECK (expiry_date IS NULL OR expiry_date >= issue_date)
);
-- -------------------------------------------------------
-- 21. patient_contacts
-- -------------------------------------------------------
CREATE TABLE patient_contacts (
contact_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
patient_id BIGINT NOT NULL,
relationship VARCHAR(30) NOT NULL,
contact_name_en VARCHAR(200) NOT NULL,
contact_name_ar VARCHAR(200),
mobile_phone VARCHAR(20) NOT NULL,
home_phone VARCHAR(20),
email VARCHAR(200),
is_emergency_contact BOOLEAN NOT NULL DEFAULT TRUE,
is_next_of_kin BOOLEAN NOT NULL DEFAULT FALSE,
priority INT NOT NULL DEFAULT 1,
created_by BIGINT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_contact_patient FOREIGN KEY (patient_id) REFERENCES patients(patient_id),
CONSTRAINT chk_contact_rel CHECK (relationship IN ('spouse','parent','child','sibling','guardian','friend','employer','other')),
CONSTRAINT chk_contact_priority CHECK (priority >= 1)
);
-- -------------------------------------------------------
-- 22. integration_message_log
-- -------------------------------------------------------
CREATE TABLE integration_message_log (
message_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
message_control_id VARCHAR(50),
direction VARCHAR(10) NOT NULL,
message_type VARCHAR(30) NOT NULL,
target_system VARCHAR(50) NOT NULL,
integration_endpoint VARCHAR(200),
patient_id BIGINT,
status VARCHAR(20) NOT NULL DEFAULT 'queued',
payload TEXT,
response_payload TEXT,
ack_code VARCHAR(5),
retry_count INT NOT NULL DEFAULT 0,
max_retries INT NOT NULL DEFAULT 10,
next_retry_at TIMESTAMP,
error_message TEXT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
sent_at TIMESTAMP,
completed_at TIMESTAMP,
CONSTRAINT fk_intmsg_patient FOREIGN KEY (patient_id) REFERENCES patients(patient_id),
CONSTRAINT chk_intmsg_dir CHECK (direction IN ('outbound','inbound')),
CONSTRAINT chk_intmsg_status CHECK (status IN ('queued','sent','pending','acknowledged','error','failed','rejected'))
);
Data Governance & UAE PDPL Compliance
Data Classification
| Classification | Examples | Access Control | Encryption | Retention |
|---|---|---|---|---|
| Sensitive Health Data | Clinical notes, allergies, problems, diagnoses | Role-based + BTG | At-rest (AES-256) + in-transit (TLS 1.2+) | 10 years minimum (DOH/DHA) |
| Personal Identifiers | Emirates ID, passport, MRN | Role-based, field-level masking | Field-level encryption (Emirates ID) | Patient lifetime + 10 years |
| Administrative | Facilities, departments, roles | Role-based | At-rest (volume level) | System lifetime |
| Audit | Audit log, BTG records | Read-only (admin/compliance) | At-rest, immutable | 10 years minimum |
| Integration | HL7 messages, FHIR payloads | System/integration accounts | In-transit (TLS/mTLS) | 2 years (configurable) |
Retention Periods
| Data Category | Minimum Retention | Legal Basis | Archival |
|---|---|---|---|
| Patient records (all clinical) | 10 years from last encounter | DOH Regulation; DHA Standards | Cold storage after 5 years |
| Consent records | Patient lifetime + 10 years | UAE PDPL Art. 7 | Never purge while patient active |
| Audit logs | 10 years | NESA/ADHICS; DOH/DHA | Partitioned; archive monthly |
| Integration messages | 2 years | Operational | Purge payload after 90 days; keep metadata 2 years |
| Scanned documents | Same as parent record | DOH/DHA | Object storage lifecycle policy |
Cross-Border Transfer Controls
Per UAE PDPL Art. 22, the following database fields may contain data subject to cross-border transfer restrictions:
patients.*,patient_demographics.*,patient_identifiers.*— all PIIpatient_allergies.*,patient_problems.*,clinical_notes.*— sensitive health datapatient_consents.*— consent records themselves
Implementation: If the HIS uses cloud hosting, ensure UAE-region data centers (Azure UAE North, AWS me-south-1, or equivalent). Implement
data_residencyflags on facility configuration to enforce geographic restrictions.