Patient Portal & Mobile App Data Specifications
Shared Entity References
This module references the following shared entities defined in their owning modules. It does not redefine these tables — it uses foreign-key references only.
| Shared Entity | Owning Module | Table(s) | FK Used Here |
|---|---|---|---|
| Patients | ehr-patient-mgmt |
patients |
patients.patient_id |
| Patient Demographics | ehr-patient-mgmt |
patient_demographics |
patient_demographics.patient_id |
| Patient Identifiers | ehr-patient-mgmt |
patient_identifiers |
patient_identifiers.patient_id |
| Providers | ehr-patient-mgmt |
providers, provider_credentials |
providers.provider_id |
| Users & Auth | ehr-patient-mgmt |
users, roles, permissions |
users.user_id |
| Facilities | ehr-patient-mgmt |
facilities, departments, locations |
facilities.facility_id |
| Encounters | scheduling |
encounters, encounter_details |
encounters.encounter_id |
| Appointments | scheduling |
appointments |
appointments.appointment_id |
| Patient Allergies | ehr-patient-mgmt |
patient_allergies |
patient_allergies.allergy_id |
| Patient Problems | ehr-patient-mgmt |
patient_problems |
patient_problems.problem_id |
| Patient Documents | ehr-patient-mgmt |
patient_documents |
patient_documents.document_id |
| Patient Consents | ehr-patient-mgmt |
patient_consents |
patient_consents.consent_id |
| Lab Results | lis |
lab_results |
lab_results.result_id |
| Radiology Reports | ris |
radiology_reports |
radiology_reports.report_id |
| Pharmacy Orders | pis |
pharmacy_orders |
pharmacy_orders.order_id |
| Patient Invoices | billing-claims |
patient_invoices |
patient_invoices.invoice_id |
| Payers | policy-contract-mgmt |
payers, insurance_plans |
payers.payer_id |
Entity Relationship Diagram
Table Definitions
1. portal_accounts
Purpose
Stores patient-facing portal accounts for web and mobile access. Links to the core patient record and user authentication record. Supports UAE Pass linkage, bilingual preferences, and MFA configuration.
Field Specification
| Field | Type | Nullable | Default | Constraint | Description |
|---|---|---|---|---|---|
account_id |
BIGINT | NO | IDENTITY | PK | Portal account identifier |
patient_id |
BIGINT | NO | — | FK → patients.patient_id |
Linked patient |
user_id |
BIGINT | NO | — | FK → users.user_id |
Auth user record |
email |
VARCHAR(254) | NO | — | UNIQUE | Login / notification email |
phone |
VARCHAR(20) | YES | NULL | — | Mobile number (+9715XXXXXXXX) |
activation_status |
VARCHAR(20) | NO | 'pending' |
IN ('pending','active','locked','disabled') |
Account lifecycle state |
activation_date |
TIMESTAMP | YES | NULL | — | When first activated |
last_login |
TIMESTAMP | YES | NULL | — | Last successful login |
mfa_method |
VARCHAR(30) | YES | NULL | IN ('sms_otp','email_otp','auth_app','biometric') |
Preferred MFA |
uae_pass_linked |
BOOLEAN | NO | FALSE | — | UAE Pass SSO linked |
uae_pass_sub |
VARCHAR(100) | YES | NULL | — | UAE Pass subject identifier |
language_preference |
VARCHAR(10) | NO | 'en' |
IN ('en','ar') |
UI language |
time_zone |
VARCHAR(50) | NO | 'Asia/Dubai' |
— | For reminders, display |
is_active |
BOOLEAN | NO | TRUE | — | Soft delete flag |
created_at |
TIMESTAMP | NO | CURRENT_TIMESTAMP | — | Record creation |
created_by |
BIGINT | YES | NULL | FK → users.user_id |
Creator (staff or system) |
updated_at |
TIMESTAMP | NO | CURRENT_TIMESTAMP | — | Last update |
updated_by |
BIGINT | YES | NULL | FK → users.user_id |
Last updater |
SQL DDL
CREATE TABLE portal_accounts (
account_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
patient_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
email VARCHAR(254) NOT NULL,
phone VARCHAR(20),
activation_status VARCHAR(20) NOT NULL DEFAULT 'pending',
activation_date TIMESTAMP,
last_login TIMESTAMP,
mfa_method VARCHAR(30),
uae_pass_linked BOOLEAN NOT NULL DEFAULT FALSE,
uae_pass_sub VARCHAR(100),
language_preference VARCHAR(10) NOT NULL DEFAULT 'en',
time_zone VARCHAR(50) NOT NULL DEFAULT 'Asia/Dubai',
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
created_by BIGINT,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_by BIGINT,
CONSTRAINT uq_portal_accounts_email UNIQUE (email),
CONSTRAINT fk_portal_accounts_patient
FOREIGN KEY (patient_id) REFERENCES patients(patient_id)
ON UPDATE CASCADE ON DELETE RESTRICT,
CONSTRAINT fk_portal_accounts_user
FOREIGN KEY (user_id) REFERENCES users(user_id)
ON UPDATE CASCADE ON DELETE RESTRICT,
CONSTRAINT fk_portal_accounts_created_by
FOREIGN KEY (created_by) REFERENCES users(user_id)
ON UPDATE CASCADE ON DELETE SET NULL,
CONSTRAINT fk_portal_accounts_updated_by
FOREIGN KEY (updated_by) REFERENCES users(user_id)
ON UPDATE CASCADE ON DELETE SET NULL,
CONSTRAINT chk_portal_accounts_activation_status
CHECK (activation_status IN ('pending','active','locked','disabled')),
CONSTRAINT chk_portal_accounts_language
CHECK (language_preference IN ('en','ar')),
CONSTRAINT chk_portal_accounts_mfa_method
CHECK (mfa_method IS NULL OR mfa_method IN ('sms_otp','email_otp','auth_app','biometric'))
);
CREATE INDEX idx_portal_accounts_patient
ON portal_accounts(patient_id);
CREATE INDEX idx_portal_accounts_user
ON portal_accounts(user_id);
CREATE INDEX idx_portal_accounts_status
ON portal_accounts(activation_status)
WHERE is_active = TRUE;
COMMENT ON TABLE portal_accounts IS 'Patient portal user accounts for web/mobile access (UAE PDPL-compliant).';
COMMENT ON COLUMN portal_accounts.uae_pass_sub IS 'Opaque subject identifier from UAE Pass (OAuth2/OIDC).';
Terminology Bindings
| Field | Terminology | Example Value |
|---|---|---|
language_preference |
BCP-47 language tags (facility subset) | en, ar |
time_zone |
IANA Time Zone Database | Asia/Dubai |
FHIR Resource Mapping
| Table Field | FHIR Resource | FHIR Path |
|---|---|---|
account_id |
Patient (extension) | Patient.extension[url='.../portal-account-id'].valueIdentifier.value |
patient_id |
Patient | Patient.id |
email |
Patient | Patient.telecom[system='email'].value |
phone |
Patient | Patient.telecom[system='phone'].value |
language_preference |
Patient | Patient.communication.language.coding.code |
uae_pass_sub |
Patient (extension) | Patient.extension[url='.../uae-pass-sub'].valueString |
2. portal_sessions
Purpose
Tracks login sessions for auditing, cybersecurity (ADHICS / NABIDH), and KPI calculation (MAU).
Field Specification
| Field | Type | Nullable | Default | Constraint | Description |
|---|---|---|---|---|---|
session_id |
BIGINT | NO | IDENTITY | PK | Session identifier |
account_id |
BIGINT | NO | — | FK → portal_accounts.account_id |
Portal account |
login_datetime |
TIMESTAMP | NO | CURRENT_TIMESTAMP | — | Login time |
logout_datetime |
TIMESTAMP | YES | NULL | ≥ login_datetime |
Logout time |
device_type |
VARCHAR(30) | YES | NULL | IN ('web','ios','android','other') |
Client type |
device_os |
VARCHAR(100) | YES | NULL | — | OS/version |
ip_address |
INET | YES | NULL | — | Client IP (per TDRA/NESA) |
auth_method |
VARCHAR(30) | YES | NULL | IN ('password','uae_pass','biometric') |
Primary auth |
mfa_used |
BOOLEAN | NO | FALSE | — | Whether MFA used |
created_at |
TIMESTAMP | NO | CURRENT_TIMESTAMP | — | Record creation |
SQL DDL
CREATE TABLE portal_sessions (
session_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
account_id BIGINT NOT NULL,
login_datetime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
logout_datetime TIMESTAMP,
device_type VARCHAR(30),
device_os VARCHAR(100),
ip_address INET,
auth_method VARCHAR(30),
mfa_used BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_portal_sessions_account
FOREIGN KEY (account_id) REFERENCES portal_accounts(account_id)
ON UPDATE CASCADE ON DELETE CASCADE,
CONSTRAINT chk_portal_sessions_device_type
CHECK (device_type IS NULL OR device_type IN ('web','ios','android','other')),
CONSTRAINT chk_portal_sessions_auth_method
CHECK (auth_method IS NULL OR auth_method IN ('password','uae_pass','biometric')),
CONSTRAINT chk_portal_sessions_logout_after_login
CHECK (logout_datetime IS NULL OR logout_datetime >= login_datetime)
);
CREATE INDEX idx_portal_sessions_account
ON portal_sessions(account_id);
CREATE INDEX idx_portal_sessions_login_datetime
ON portal_sessions(login_datetime);
CREATE INDEX idx_portal_sessions_recent_active
ON portal_sessions(account_id, login_datetime)
WHERE logout_datetime IS NULL;
Terminology Bindings
Internal enumerations only.
FHIR Resource Mapping
Audit-related; not a core clinical resource. When exposed via FHIR AuditEvent:
| Table Field | FHIR Resource | FHIR Path |
|---|---|---|
session_id |
AuditEvent | AuditEvent.id |
account_id |
AuditEvent | AuditEvent.agent.who.reference (Patient/RelatedPerson) |
login_datetime |
AuditEvent | AuditEvent.recorded |
ip_address |
AuditEvent | AuditEvent.source.observer.extension[ip] |
3. portal_messages
Purpose
Stores secure, non-emergency messaging between patients and providers/clinical teams. Messages are part of the medico-legal record and must be retained per UAE regulations.
Field Specification
| Field | Type | Nullable | Default | Constraint | Description |
|---|---|---|---|---|---|
message_id |
BIGINT | NO | IDENTITY | PK | Message identifier |
thread_id |
BIGINT | NO | — | — | Conversation/thread grouping |
sender_account_id |
BIGINT | YES | NULL | FK → portal_accounts.account_id |
Patient sender (if patient) |
sender_type |
VARCHAR(20) | NO | — | IN ('patient','provider','system') |
Sender role |
recipient_provider_id |
BIGINT | YES | NULL | FK → providers.provider_id |
Provider recipient (if provider) |
recipient_account_id |
BIGINT | YES | NULL | FK → portal_accounts.account_id |
Patient recipient (if patient) |
recipient_type |
VARCHAR(20) | NO | — | IN ('patient','provider','team') |
Recipient role |
subject |
VARCHAR(255) | NO | — | — | Message subject |
body |
TEXT | NO | — | — | Message content |
attachments |
JSONB | YES | NULL | — | Array of attachment metadata |
sent_datetime |
TIMESTAMP | NO | CURRENT_TIMESTAMP | — | When sent |
read_datetime |
TIMESTAMP | YES | NULL | ≥ sent_datetime |
When first read |
is_urgent |
BOOLEAN | NO | FALSE | — | Marked urgent (with warnings) |
encounter_id |
BIGINT | YES | NULL | FK → encounters.encounter_id |
Related encounter |
created_at |
TIMESTAMP | NO | CURRENT_TIMESTAMP | — | Record creation |
Attachment JSON structure (example):
[
{
"document_id": 12345,
"file_name": "bp_readings.pdf",
"content_type": "application/pdf",
"size_bytes": 45231
}
]
SQL DDL
CREATE TABLE portal_messages (
message_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
thread_id BIGINT NOT NULL,
sender_account_id BIGINT,
sender_type VARCHAR(20) NOT NULL,
recipient_provider_id BIGINT,
recipient_account_id BIGINT,
recipient_type VARCHAR(20) NOT NULL,
subject VARCHAR(255) NOT NULL,
body TEXT NOT NULL,
attachments JSONB,
sent_datetime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
read_datetime TIMESTAMP,
is_urgent BOOLEAN NOT NULL DEFAULT FALSE,
encounter_id BIGINT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_portal_messages_sender_account
FOREIGN KEY (sender_account_id) REFERENCES portal_accounts(account_id)
ON UPDATE CASCADE ON DELETE SET NULL,
CONSTRAINT fk_portal_messages_recipient_account
FOREIGN KEY (recipient_account_id) REFERENCES portal_accounts(account_id)
ON UPDATE CASCADE ON DELETE SET NULL,
CONSTRAINT fk_portal_messages_recipient_provider
FOREIGN KEY (recipient_provider_id) REFERENCES providers(provider_id)
ON UPDATE CASCADE ON DELETE SET NULL,
CONSTRAINT fk_portal_messages_encounter
FOREIGN KEY (encounter_id) REFERENCES encounters(encounter_id)
ON UPDATE CASCADE ON DELETE SET NULL,
CONSTRAINT chk_portal_messages_sender_type
CHECK (sender_type IN ('patient','provider','system')),
CONSTRAINT chk_portal_messages_recipient_type
CHECK (recipient_type IN ('patient','provider','team')),
CONSTRAINT chk_portal_messages_read_after_sent
CHECK (read_datetime IS NULL OR read_datetime >= sent_datetime),
CONSTRAINT chk_portal_messages_sender_recipient
CHECK (
-- at least one recipient
(recipient_provider_id IS NOT NULL OR recipient_account_id IS NOT NULL)
)
);
CREATE INDEX idx_portal_messages_thread
ON portal_messages(thread_id, sent_datetime);
CREATE INDEX idx_portal_messages_recipient_provider_unread
ON portal_messages(recipient_provider_id, sent_datetime)
WHERE read_datetime IS NULL;
CREATE INDEX idx_portal_messages_recipient_account_unread
ON portal_messages(recipient_account_id, sent_datetime)
WHERE read_datetime IS NULL;
CREATE INDEX idx_portal_messages_encounter
ON portal_messages(encounter_id);
Terminology Bindings
Internal enumerations only.
FHIR Resource Mapping
Maps to FHIR Communication:
| Table Field | FHIR Resource | FHIR Path |
|---|---|---|
message_id |
Communication | Communication.id |
sender_account_id |
Communication | Communication.sender (Reference to Patient/RelatedPerson) |
recipient_provider_id |
Communication | Communication.recipient (Practitioner) |
subject |
Communication | Communication.topic.text |
body |
Communication | Communication.payload.contentString |
attachments |
Communication | Communication.payload.contentAttachment (via patient_documents) |
sent_datetime |
Communication | Communication.sent |
read_datetime |
Communication | Communication.received |
encounter_id |
Communication | Communication.encounter |
4. portal_notifications
Purpose
Tracks notifications (push, SMS, email, in-app) sent to portal users for appointments, results, messages, billing, and security events. Supports audit and troubleshooting.
Field Specification
| Field | Type | Nullable | Default | Constraint | Description |
|---|---|---|---|---|---|
notification_id |
BIGINT | NO | IDENTITY | PK | Notification identifier |
account_id |
BIGINT | NO | — | FK → portal_accounts.account_id |
Recipient account |
notification_type |
VARCHAR(50) | NO | — | e.g. appointment, result, billing, security |
Category |
channel |
VARCHAR(20) | NO | — | IN ('in_app','email','sms','push') |
Delivery channel |
subject |
VARCHAR(255) | NO | — | — | Notification title |
body |
TEXT | NO | — | — | Notification body (localized) |
payload |
JSONB | YES | NULL | — | Structured metadata (e.g. appointment_id) |
sent_datetime |
TIMESTAMP | NO | CURRENT_TIMESTAMP | — | When sent |
read_datetime |
TIMESTAMP | YES | NULL | ≥ sent_datetime |
When opened (in-app) |
action_url |
VARCHAR(500) | YES | NULL | — | Deep link / URL |
status |
VARCHAR(20) | NO | 'pending' |
IN ('pending','sent','failed','read') |
Delivery status |
error_message |
VARCHAR(500) | YES | NULL | — | Last error if failed |
SQL DDL
CREATE TABLE portal_notifications (
notification_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
account_id BIGINT NOT NULL,
notification_type VARCHAR(50) NOT NULL,
channel VARCHAR(20) NOT NULL,
subject VARCHAR(255) NOT NULL,
body TEXT NOT NULL,
payload JSONB,
sent_datetime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
read_datetime TIMESTAMP,
action_url VARCHAR(500),
status VARCHAR(20) NOT NULL DEFAULT 'pending',
error_message VARCHAR(500),
CONSTRAINT fk_portal_notifications_account
FOREIGN KEY (account_id) REFERENCES portal_accounts(account_id)
ON UPDATE CASCADE ON DELETE CASCADE,
CONSTRAINT chk_portal_notifications_channel
CHECK (channel IN ('in_app','email','sms','push')),
CONSTRAINT chk_portal_notifications_status
CHECK (status IN ('pending','sent','failed','read')),
CONSTRAINT chk_portal_notifications_read_after_sent
CHECK (read_datetime IS NULL OR read_datetime >= sent_datetime)
);
CREATE INDEX idx_portal_notifications_account
ON portal_notifications(account_id, sent_datetime);
CREATE INDEX idx_portal_notifications_status
ON portal_notifications(status);
CREATE INDEX idx_portal_notifications_unread_inapp
ON portal_notifications(account_id, sent_datetime)
WHERE channel = 'in_app' AND read_datetime IS NULL;
Terminology Bindings
Internal enumerations only.
FHIR Resource Mapping
No direct core mapping; when exposed, can be represented as Communication or Task depending on type. Example:
| Table Field | FHIR Resource | FHIR Path |
|---|---|---|
notification_id |
Communication | Communication.id |
notification_type |
Communication | Communication.category |
sent_datetime |
Communication | Communication.sent |
5. portal_preferences
Purpose
Stores patient-specific communication and display preferences, including notification channels, language, and result release preferences (within bounds of facility policy and UAE PDPL consent).
Field Specification
| Field | Type | Nullable | Default | Constraint | Description |
|---|---|---|---|---|---|
preference_id |
BIGINT | NO | IDENTITY | PK | Preference record |
account_id |
BIGINT | NO | — | FK → portal_accounts.account_id |
Owner account |
notification_email |
BOOLEAN | NO | TRUE | — | Allow email notifications |
notification_sms |
BOOLEAN | NO | FALSE | — | Allow SMS notifications |
notification_push |
BOOLEAN | NO | TRUE | — | Allow push notifications |
language |
VARCHAR(10) | NO | 'en' |
IN ('en','ar') |
Preferred language |
display_theme |
VARCHAR(20) | NO | 'light' |
IN ('light','dark','system') |
UI theme |
result_release_preference |
VARCHAR(20) | YES | NULL | IN ('immediate','delayed') |
Patient preference (subject to policy) |
marketing_opt_in |
BOOLEAN | NO | FALSE | — | Non-clinical communications consent |
created_at |
TIMESTAMP | NO | CURRENT_TIMESTAMP | — | Creation time |
updated_at |
TIMESTAMP | NO | CURRENT_TIMESTAMP | — | Last update |
SQL DDL
CREATE TABLE portal_preferences (
preference_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
account_id BIGINT NOT NULL UNIQUE,
notification_email BOOLEAN NOT NULL DEFAULT TRUE,
notification_sms BOOLEAN NOT NULL DEFAULT FALSE,
notification_push BOOLEAN NOT NULL DEFAULT TRUE,
language VARCHAR(10) NOT NULL DEFAULT 'en',
display_theme VARCHAR(20) NOT NULL DEFAULT 'light',
result_release_preference VARCHAR(20),
marketing_opt_in BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_portal_preferences_account
FOREIGN KEY (account_id) REFERENCES portal_accounts(account_id)
ON UPDATE CASCADE ON DELETE CASCADE,
CONSTRAINT chk_portal_preferences_language
CHECK (language IN ('en','ar')),
CONSTRAINT chk_portal_preferences_theme
CHECK (display_theme IN ('light','dark','system')),
CONSTRAINT chk_portal_preferences_result_release
CHECK (result_release_preference IS NULL OR result_release_preference IN ('immediate','delayed'))
);
CREATE INDEX idx_portal_preferences_language
ON portal_preferences(language);
Terminology Bindings
| Field | Terminology | Example Value |
|---|---|---|
language |
BCP-47 | en, ar |
FHIR Resource Mapping
Maps to Patient.communication and extensions:
| Table Field | FHIR Resource | FHIR Path |
|---|---|---|
language |
Patient | Patient.communication.language.coding.code |
notification_email |
Patient (extension) | Patient.extension[url='.../notify-email'].valueBoolean |
notification_sms |
Patient (extension) | Patient.extension[url='.../notify-sms'].valueBoolean |
6. telehealth_sessions
Purpose
Tracks telehealth video sessions initiated from portal/mobile, including join times, platform, connection quality, and linkage to appointments and encounters. Supports billing, quality metrics, and PDPL-compliant audit.
Field Specification
| Field | Type | Nullable | Default | Constraint | Description |
|---|---|---|---|---|---|
session_id |
BIGINT | NO | IDENTITY | PK | Telehealth session identifier |
appointment_id |
BIGINT | NO | — | FK → appointments.appointment_id |
Scheduled appointment |
patient_account_id |
BIGINT | NO | — | FK → portal_accounts.account_id |
Patient portal account |
provider_id |
BIGINT | NO | — | FK → providers.provider_id |
Consulting provider |
scheduled_datetime |
TIMESTAMP | NO | — | — | Scheduled start time |
join_datetime_patient |
TIMESTAMP | YES | NULL | ≥ scheduled_datetime |
Patient join time |
join_datetime_provider |
TIMESTAMP | YES | NULL | ≥ scheduled_datetime |
Provider join time |
end_datetime |
TIMESTAMP | YES | NULL | ≥ join times | Session end time |
duration_minutes |
INT | YES | NULL | ≥ 0 | Calculated duration |
connection_quality |
VARCHAR(20) | YES | NULL | IN ('excellent','good','fair','poor') |
Overall quality |
platform |
VARCHAR(30) | NO | — | IN ('webrtc','native_ios','native_android','other') |
Technology used |
recording_consent |
BOOLEAN | NO | FALSE | — | Patient consent for recording |
recording_stored |
BOOLEAN | NO | FALSE | — | Whether recording stored |
encounter_id |
BIGINT | YES | NULL | FK → encounters.encounter_id |
Clinical encounter record |
created_at |
TIMESTAMP | NO | CURRENT_TIMESTAMP | — | Creation time |
SQL DDL
CREATE TABLE telehealth_sessions (
session_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
appointment_id BIGINT NOT NULL,
patient_account_id BIGINT NOT NULL,
provider_id BIGINT NOT NULL,
scheduled_datetime TIMESTAMP NOT NULL,
join_datetime_patient TIMESTAMP,
join_datetime_provider TIMESTAMP,
end_datetime TIMESTAMP,
duration_minutes INT,
connection_quality VARCHAR(20),
platform VARCHAR(30) NOT NULL,
recording_consent BOOLEAN NOT NULL DEFAULT FALSE,
recording_stored BOOLEAN NOT NULL DEFAULT FALSE,
encounter_id BIGINT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_telehealth_sessions_appointment
FOREIGN KEY (appointment_id) REFERENCES appointments(appointment_id)
ON UPDATE CASCADE ON DELETE RESTRICT,
CONSTRAINT fk_telehealth_sessions_patient_account
FOREIGN KEY (patient_account_id) REFERENCES portal_accounts(account_id)
ON UPDATE CASCADE ON DELETE RESTRICT,
CONSTRAINT fk_telehealth_sessions_provider
FOREIGN KEY (provider_id) REFERENCES providers(provider_id)
ON UPDATE CASCADE ON DELETE RESTRICT,
CONSTRAINT fk_telehealth_sessions_encounter
FOREIGN KEY (encounter_id) REFERENCES encounters(encounter_id)
ON UPDATE CASCADE ON DELETE SET NULL,
CONSTRAINT chk_telehealth_sessions_platform
CHECK (platform IN ('webrtc','native_ios','native_android','other')),
CONSTRAINT chk_telehealth_sessions_quality
CHECK (connection_quality IS NULL OR connection_quality IN ('excellent','good','fair','poor')),
CONSTRAINT chk_telehealth_sessions_times
CHECK (
(join_datetime_patient IS NULL OR join_datetime_patient >= scheduled_datetime) AND
(join_datetime_provider IS NULL OR join_datetime_provider >= scheduled_datetime) AND
(end_datetime IS NULL OR
(join_datetime_patient IS NULL OR end_datetime >= join_datetime_patient) AND
(join_datetime_provider IS NULL OR end_datetime >= join_datetime_provider)
)
),
CONSTRAINT chk_telehealth_sessions_duration
CHECK (duration_minutes IS NULL OR duration_minutes >= 0)
);
CREATE INDEX idx_telehealth_sessions_appointment
ON telehealth_sessions(appointment_id);
CREATE INDEX idx_telehealth_sessions_provider
ON telehealth_sessions(provider_id, scheduled_datetime);
CREATE INDEX idx_telehealth_sessions_patient
ON telehealth_sessions(patient_account_id, scheduled_datetime);
Terminology Bindings
Internal enumerations only.
FHIR Resource Mapping
Maps primarily to Encounter (telehealth) and Appointment:
| Table Field | FHIR Resource | FHIR Path |
|---|---|---|
session_id |
Encounter (extension) | Encounter.extension[url='.../telehealth-session-id'].valueIdentifier.value |
appointment_id |
Appointment | Appointment.id |
provider_id |
Encounter | Encounter.participant.individual (Practitioner) |
scheduled_datetime |
Appointment | Appointment.start |
join_datetime_patient |
Encounter | Encounter.actualPeriod.start |
end_datetime |
Encounter | Encounter.actualPeriod.end |
7. patient_submitted_forms
Purpose
Stores forms and consents submitted by patients via portal/mobile (pre-registration, medical history, PDPL consent, clinical questionnaires). Data is JSON to support dynamic templates; key fields are propagated into core EHR modules.
Field Specification
| Field | Type | Nullable | Default | Constraint | Description |
|---|---|---|---|---|---|
form_id |
BIGINT | NO | IDENTITY | PK | Submitted form identifier |
account_id |
BIGINT | NO | — | FK → portal_accounts.account_id |
Submitting account |
patient_id |
BIGINT | NO | — | FK → patients.patient_id |
Patient subject |
form_type |
VARCHAR(100) | NO | — | e.g. pre_registration, consent_surgery |
Template key |
form_version |
VARCHAR(20) | YES | NULL | — | Template version |
form_data_json |
JSONB | NO | — | — | Captured answers and signatures |
submitted_datetime |
TIMESTAMP | NO | CURRENT_TIMESTAMP | — | Submission time |
processed |
BOOLEAN | NO | FALSE | — | Whether integrated into EHR |
processed_datetime |
TIMESTAMP | YES | NULL | ≥ submitted_datetime |
Processing time |
processed_by |
BIGINT | YES | NULL | FK → users.user_id |
Staff/system user |
target_module |
VARCHAR(50) | NO | — | e.g. ehr-patient-mgmt,billing-claims |
Downstream module |
source_ip |
INET | YES | NULL | — | Client IP |
user_agent |
VARCHAR(255) | YES | NULL | — | Browser/app agent |
SQL DDL
CREATE TABLE patient_submitted_forms (
form_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
account_id BIGINT NOT NULL,
patient_id BIGINT NOT NULL,
form_type VARCHAR(100) NOT NULL,
form_version VARCHAR(20),
form_data_json JSONB NOT NULL,
submitted_datetime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
processed BOOLEAN NOT NULL DEFAULT FALSE,
processed_datetime TIMESTAMP,
processed_by BIGINT,
target_module VARCHAR(50) NOT NULL,
source_ip INET,
user_agent VARCHAR(255),
CONSTRAINT fk_patient_forms_account
FOREIGN KEY (account_id) REFERENCES portal_accounts(account_id)
ON UPDATE CASCADE ON DELETE RESTRICT,
CONSTRAINT fk_patient_forms_patient
FOREIGN KEY (patient_id) REFERENCES patients(patient_id)
ON UPDATE CASCADE ON DELETE RESTRICT,
CONSTRAINT fk_patient_forms_processed_by
FOREIGN KEY (processed_by) REFERENCES users(user_id)
ON UPDATE CASCADE ON DELETE SET NULL,
CONSTRAINT chk_patient_forms_processed_time
CHECK (processed_datetime IS NULL OR processed_datetime >= submitted_datetime)
);
CREATE INDEX idx_patient_forms_patient
ON patient_submitted_forms(patient_id, submitted_datetime);
CREATE INDEX idx_patient_forms_type
ON patient_submitted_forms(form_type, submitted_datetime);
CREATE INDEX idx_patient_forms_processed
ON patient_submitted_forms(processed, target_module);
Terminology Bindings
| Field | Terminology | Example Value |
|---|---|---|
form_type |
Local form template catalog | pre_registration, pdpl_consent, anesthesia_consent |
FHIR Resource Mapping
Depending on form type, maps to different resources (e.g. QuestionnaireResponse, Consent):
| Table Field | FHIR Resource | FHIR Path |
|---|---|---|
form_id |
QuestionnaireResponse | QuestionnaireResponse.id |
patient_id |
QuestionnaireResponse | QuestionnaireResponse.subject |
form_type |
QuestionnaireResponse | QuestionnaireResponse.questionnaire (canonical URL) |
form_data_json |
QuestionnaireResponse | QuestionnaireResponse.item (transformed) |
For consent forms:
| Table Field | FHIR Resource | FHIR Path |
|---|---|---|
form_id |
Consent | Consent.id |
patient_id |
Consent | Consent.patient |
form_type |
Consent | Consent.category |
8. portal_feedback
Purpose
Captures patient satisfaction and experience feedback related to portal usage or specific encounters (including telehealth). Used for quality improvement and KPIs.
Field Specification
| Field | Type | Nullable | Default | Constraint | Description |
|---|---|---|---|---|---|
feedback_id |
BIGINT | NO | IDENTITY | PK | Feedback identifier |
account_id |
BIGINT | NO | — | FK → portal_accounts.account_id |
Submitting account |
feedback_type |
VARCHAR(50) | NO | — | IN ('portal','telehealth','visit','billing') |
Context |
encounter_id |
BIGINT | YES | NULL | FK → encounters.encounter_id |
Related encounter (if any) |
rating |
INT | NO | — | 1–5 | Numeric rating |
comments |
TEXT | YES | NULL | — | Free-text comments |
submitted_datetime |
TIMESTAMP | NO | CURRENT_TIMESTAMP | — | Submission time |
follow_up_required |
BOOLEAN | NO | FALSE | — | Flag for service recovery |
created_at |
TIMESTAMP | NO | CURRENT_TIMESTAMP | — | Creation time |
SQL DDL
CREATE TABLE portal_feedback (
feedback_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
account_id BIGINT NOT NULL,
feedback_type VARCHAR(50) NOT NULL,
encounter_id BIGINT,
rating INT NOT NULL,
comments TEXT,
submitted_datetime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
follow_up_required BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_portal_feedback_account
FOREIGN KEY (account_id) REFERENCES portal_accounts(account_id)
ON UPDATE CASCADE ON DELETE CASCADE,
CONSTRAINT fk_portal_feedback_encounter
FOREIGN KEY (encounter_id) REFERENCES encounters(encounter_id)
ON UPDATE CASCADE ON DELETE SET NULL,
CONSTRAINT chk_portal_feedback_type
CHECK (feedback_type IN ('portal','telehealth','visit','billing')),
CONSTRAINT chk_portal_feedback_rating
CHECK (rating BETWEEN 1 AND 5)
);
CREATE INDEX idx_portal_feedback_account
ON portal_feedback(account_id, submitted_datetime);
CREATE INDEX idx_portal_feedback_type
ON portal_feedback(feedback_type, submitted_datetime);
Terminology Bindings
Internal enumerations only.
FHIR Resource Mapping
Can be represented as QuestionnaireResponse or Observation (patient experience):
| Table Field | FHIR Resource | FHIR Path |
|---|---|---|
feedback_id |
QuestionnaireResponse | QuestionnaireResponse.id |
rating |
Observation | Observation.valueInteger (patient satisfaction score) |
9. proxy_access_grants
Purpose
Manages proxy access (e.g. parent/guardian) to dependent patient records, including relationship, access level, and expiry. Must comply with UAE guardianship rules and PDPL consent.
Field Specification
| Field | Type | Nullable | Default | Constraint | Description |
|---|---|---|---|---|---|
grant_id |
BIGINT | NO | IDENTITY | PK | Proxy grant identifier |
grantor_patient_id |
BIGINT | NO | — | FK → patients.patient_id |
Patient granting access (if capable) or legal owner |
proxy_account_id |
BIGINT | NO | — | FK → portal_accounts.account_id |
Proxy portal account |
dependent_patient_id |
BIGINT | NO | — | FK → patients.patient_id |
Dependent patient |
relationship |
VARCHAR(50) | NO | — | e.g. parent, guardian, spouse |
Relationship type |
access_level |
VARCHAR(20) | NO | 'full' |
IN ('full','limited','appointments_only') |
Scope of access |
granted_datetime |
TIMESTAMP | NO | CURRENT_TIMESTAMP | — | When granted |
expiry_datetime |
TIMESTAMP | YES | NULL | ≥ granted_datetime |
When access ends |
is_active |
BOOLEAN | NO | TRUE | — | Active flag |
created_by |
BIGINT | YES | NULL | FK → users.user_id |
Staff who configured (if not self-service) |
created_at |
TIMESTAMP | NO | CURRENT_TIMESTAMP | — | Creation time |
revoked_datetime |
TIMESTAMP | YES | NULL | ≥ granted_datetime |
When revoked |
revoked_reason |
VARCHAR(255) | YES | NULL | — | Reason for revocation |
SQL DDL
CREATE TABLE proxy_access_grants (
grant_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
grantor_patient_id BIGINT NOT NULL,
proxy_account_id BIGINT NOT NULL,
dependent_patient_id BIGINT NOT NULL,
relationship VARCHAR(50) NOT NULL,
access_level VARCHAR(20) NOT NULL DEFAULT 'full',
granted_datetime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
expiry_datetime TIMESTAMP,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_by BIGINT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
revoked_datetime TIMESTAMP,
revoked_reason VARCHAR(255),
CONSTRAINT fk_proxy_grants_grantor_patient
FOREIGN KEY (grantor_patient_id) REFERENCES patients(patient_id)
ON UPDATE CASCADE ON DELETE RESTRICT,
CONSTRAINT fk_proxy_grants_proxy_account
FOREIGN KEY (proxy_account_id) REFERENCES portal_accounts(account_id)
ON UPDATE CASCADE ON DELETE CASCADE,
CONSTRAINT fk_proxy_grants_dependent_patient
FOREIGN KEY (dependent_patient_id) REFERENCES patients(patient_id)
ON UPDATE CASCADE ON DELETE RESTRICT,
CONSTRAINT fk_proxy_grants_created_by
FOREIGN KEY (created_by) REFERENCES users(user_id)
ON UPDATE CASCADE ON DELETE SET NULL,
CONSTRAINT chk_proxy_grants_access_level
CHECK (access_level IN ('full','limited','appointments_only')),
CONSTRAINT chk_proxy_grants_expiry
CHECK (expiry_datetime IS NULL OR expiry_datetime >= granted_datetime),
CONSTRAINT chk_proxy_grants_revoked_time
CHECK (revoked_datetime IS NULL OR revoked_datetime >= granted_datetime),
CONSTRAINT uq_proxy_grants_unique
UNIQUE (proxy_account_id, dependent_patient_id, access_level)
);
CREATE INDEX idx_proxy_grants_proxy
ON proxy_access_grants(proxy_account_id, is_active);
CREATE INDEX idx_proxy_grants_dependent
ON proxy_access_grants(dependent_patient_id, is_active);
Terminology Bindings
| Field | Terminology | Example Value |
|---|---|---|
relationship |
SNOMED CT / local | parent, guardian, spouse |
FHIR Resource Mapping
Maps to FHIR RelatedPerson and Consent:
| Table Field | FHIR Resource | FHIR Path |
|---|---|---|
proxy_account_id |
RelatedPerson | RelatedPerson.id (linked to Patient) |
dependent_patient_id |
RelatedPerson | RelatedPerson.patient |
relationship |
RelatedPerson | RelatedPerson.relationship |
access_level |
Consent | Consent.provision.type / Consent.provision.class |
expiry_datetime |
Consent | Consent.period.end |
Data Volume Estimates
Estimates are for a medium-to-large UAE hospital group (e.g. 300–500 beds, 250k active patients).
| Table | Initial Rows (Go-Live) | Annual Growth | Notes |
|---|---|---|---|
portal_accounts |
50,000 | +30,000/year | Adoption ramp; target ≥70% of active patients by year 2 |
portal_sessions |
0 | ~3,000,000/year | Assume avg 5 logins/year per active account |
portal_messages |
0 | ~400,000/year | 1–2 threads per patient/year, multi-message threads |
portal_notifications |
0 | ~5,000,000/year | High volume due to reminders, results, billing |
portal_preferences |
50,000 | +30,000/year | 1:1 with portal_accounts |
telehealth_sessions |
0 | ~25,000/year | Assuming 10–15% of OP visits telehealth |
patient_submitted_forms |
0 | ~300,000/year | Pre-registration + consents + questionnaires |
portal_feedback |
0 | ~80,000/year | Post-visit and portal NPS prompts |
proxy_access_grants |
5,000 | +3,000/year | Paediatrics and dependent adults |
Storage planning should consider 7–10 years of online data for high-volume tables (portal_sessions, portal_notifications), with older data archived to cheaper storage while maintaining PDPL-compliant access controls.
Data Retention Policy (UAE Context)
Retention must align with UAE federal and emirate-level health regulations, medical liability laws, and UAE PDPL (Federal Decree-Law No. 45/2021). Where specific durations are not prescribed, align with facility medical record retention (often 10+ years for adults, longer for paediatrics) and DOH/DHA guidance.
| Table | Recommended Retention | Rationale / Notes |
|---|---|---|
portal_accounts |
Retain for lifetime of patient record + 10 years after deactivation | Part of identity and access history; supports medico-legal traceability and PDPL accountability. |
portal_sessions |
Detailed logs online for 2 years; archive for minimum 10 years | Security/audit logs per ADHICS/NESA; older data may be archived but must be retrievable for investigations. |
portal_messages |
Same as core medical record (≥10 years after last encounter; longer for minors) | Clinical communication is part of the legal health record. For paediatrics, retain at least until patient age 25 or per MOH/DOH/DHA policy. |
portal_notifications |
2 years online; archive or purge after 5 years | Notifications are delivery artifacts; underlying clinical/billing data retained elsewhere. Keep enough for dispute resolution. |
portal_preferences |
For lifetime of portal account + 2 years | Non-clinical; retain while account exists to evidence PDPL consent choices. |
telehealth_sessions |
Metadata: same as encounters (≥10 years); recordings (if any) per facility policy (often 5–10 years) | Telehealth is equivalent to a visit; metadata must align with encounter retention. Recording retention must be clearly disclosed in PDPL consent. |
patient_submitted_forms |
Same as associated clinical record (≥10 years; longer for minors) | Forms and consents are part of the medical record; retention aligned with patient_consents and documentation policies. |
portal_feedback |
5 years | Quality improvement and KPI analytics; not typically part of legal record unless escalated. Anonymisation/pseudonymisation recommended for long-term analytics. |
proxy_access_grants |
For duration of proxy relationship + 10 years | Evidence of who had access to which records and when; important for PDPL and medico-legal defence. |
All retention and archival processes must:
- Support data subject rights under UAE PDPL (access, rectification, restriction, objection, erasure where applicable).
- Ensure secure deletion/archival with appropriate encryption and access controls (per ADHICS, NABIDH, Malaffi, TDRA/NESA).
- Be documented in the facility’s Record of Processing Activities (RoPA) and data retention schedule.