PDPL & Global Data Protection
Building healthcare applications for international markets requires understanding multiple data protection frameworks. This module covers Saudi Arabia’s PDPL, GDPR, and how they intersect with HIPAA.Global Data Protection Framework Comparison
Learning Objectives:
- Understand Saudi Arabia’s PDPL requirements
- Map HIPAA controls to PDPL compliance
- Navigate cross-border data transfers
- Implement consent management frameworks
- Handle data localization requirements
Global Data Protection Landscape
Copy
┌─────────────────────────────────────────────────────────────────────────────┐
│ GLOBAL DATA PROTECTION FRAMEWORKS │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ HIPAA (USA) PDPL (Saudi Arabia) │
│ ────────── ─────────────────── │
│ • Healthcare-specific • All personal data │
│ • PHI protection • Consent-based model │
│ • Business Associates • Data localization │
│ • 1996, updated 2013 • 2021, effective 2023 │
│ │
│ GDPR (European Union) Other Key Regulations │
│ ──────────────────── ───────────────────── │
│ • All personal data • PIPEDA (Canada) │
│ • Rights-based approach • LGPD (Brazil) │
│ • Data minimization • POPIA (South Africa) │
│ • 2018 • PDPA (Singapore, Thailand) │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Saudi Arabia’s PDPL (نظام حماية البيانات الشخصية)
Overview
The Personal Data Protection Law (PDPL) was issued by Royal Decree in September 2021 and became enforceable in 2023. It represents Saudi Arabia’s first comprehensive data protection framework.Key Definitions
Copy
class PDPLDefinitions:
"""Key terms under Saudi PDPL"""
# Personal Data (البيانات الشخصية)
PERSONAL_DATA = """
Any data, regardless of source or form, that would
identify an individual or make them identifiable,
directly or indirectly.
Includes: name, ID number, addresses, contact info,
photos, financial data, employment data, etc.
"""
# Sensitive Personal Data (البيانات الشخصية الحساسة)
SENSITIVE_DATA = """
Personal data revealing:
- Ethnic or tribal origin
- Religious, intellectual, or political beliefs
- Membership in associations
- Criminal records
- Health data (including genetic/biometric)
- Location data
- Financial data (credit, bank accounts)
"""
# Data Controller (جهة التحكم)
DATA_CONTROLLER = """
Any entity that determines the purpose and means
of processing personal data.
"""
# Data Subject (صاحب البيانات)
DATA_SUBJECT = """
The individual whose personal data is being processed.
"""
PDPL Principles
Lawfulness & Transparency
Processing must be lawful, fair, and transparent to the data subject
Purpose Limitation
Data collected for specific, explicit, and legitimate purposes
Data Minimization
Only collect data necessary for the specified purpose
Accuracy
Keep personal data accurate and up-to-date
Storage Limitation
Retain data only as long as necessary for the purpose
Security
Implement appropriate technical and organizational measures
PDPL vs HIPAA Comparison
Copy
┌─────────────────────────────────────────────────────────────────────────────┐
│ PDPL vs HIPAA COMPARISON │
├───────────────────────┬─────────────────────────┬───────────────────────────┤
│ ASPECT │ PDPL │ HIPAA │
├───────────────────────┼─────────────────────────┼───────────────────────────┤
│ Scope │ All personal data │ Healthcare data only │
├───────────────────────┼─────────────────────────┼───────────────────────────┤
│ Legal Basis │ Consent primary │ TPO without consent │
├───────────────────────┼─────────────────────────┼───────────────────────────┤
│ Data Localization │ Required (with │ No requirement │
│ │ exceptions) │ │
├───────────────────────┼─────────────────────────┼───────────────────────────┤
│ Breach Notification │ 72 hours to authority │ 60 days to individuals │
├───────────────────────┼─────────────────────────┼───────────────────────────┤
│ DPO Requirement │ Yes (for certain │ No explicit requirement │
│ │ entities) │ │
├───────────────────────┼─────────────────────────┼───────────────────────────┤
│ Penalties │ Up to 5M SAR │ Up to $1.9M per violation │
│ │ (~$1.3M USD) │ │
├───────────────────────┼─────────────────────────┼───────────────────────────┤
│ Right to Access │ Yes │ Yes │
├───────────────────────┼─────────────────────────┼───────────────────────────┤
│ Right to Erasure │ Yes │ No (record retention │
│ │ │ requirements) │
├───────────────────────┼─────────────────────────┼───────────────────────────┤
│ Consent Withdrawal │ Yes, at any time │ Limited applicability │
└───────────────────────┴─────────────────────────┴───────────────────────────┘
Implementing PDPL Compliance
Consent Management
Copy
from datetime import datetime
from typing import Optional, List
from enum import Enum
from dataclasses import dataclass, field
import uuid
class ConsentPurpose(Enum):
"""PDPL-defined processing purposes"""
HEALTHCARE_TREATMENT = "healthcare_treatment"
HEALTHCARE_PAYMENT = "healthcare_payment"
HEALTHCARE_OPERATIONS = "healthcare_operations"
RESEARCH = "research"
MARKETING = "marketing"
ANALYTICS = "analytics"
THIRD_PARTY_SHARING = "third_party_sharing"
CROSS_BORDER_TRANSFER = "cross_border_transfer"
class ConsentStatus(Enum):
GRANTED = "granted"
WITHDRAWN = "withdrawn"
EXPIRED = "expired"
PENDING = "pending"
@dataclass
class ConsentRecord:
"""PDPL-compliant consent record"""
consent_id: str = field(default_factory=lambda: str(uuid.uuid4()))
# Data subject
subject_id: str = ""
subject_identifier_type: str = "national_id" # Or passport, etc.
# Consent details
purpose: ConsentPurpose = ConsentPurpose.HEALTHCARE_TREATMENT
status: ConsentStatus = ConsentStatus.PENDING
# When and how
granted_at: Optional[datetime] = None
withdrawn_at: Optional[datetime] = None
expires_at: Optional[datetime] = None
# Consent mechanism
collection_method: str = "electronic" # electronic, written, verbal
consent_text_version: str = "" # Version of consent form
consent_language: str = "ar" # Arabic or English
# Evidence
evidence_type: str = "" # signature, checkbox, etc.
evidence_reference: str = "" # Link to stored evidence
ip_address: str = ""
user_agent: str = ""
# Scope
data_categories: List[str] = field(default_factory=list)
retention_period_days: int = 0
third_parties: List[str] = field(default_factory=list)
def is_valid(self) -> bool:
"""Check if consent is currently valid"""
if self.status != ConsentStatus.GRANTED:
return False
if self.expires_at and datetime.utcnow() > self.expires_at:
return False
return True
class ConsentManager:
"""Manage consent lifecycle for PDPL compliance"""
def __init__(self, storage, audit_logger):
self.storage = storage
self.audit_logger = audit_logger
async def request_consent(
self,
subject_id: str,
purpose: ConsentPurpose,
consent_text: str,
data_categories: List[str],
third_parties: List[str] = None,
retention_days: int = 365,
) -> ConsentRecord:
"""Create a pending consent request"""
record = ConsentRecord(
subject_id=subject_id,
purpose=purpose,
status=ConsentStatus.PENDING,
consent_text_version=self._hash_consent_text(consent_text),
data_categories=data_categories,
third_parties=third_parties or [],
retention_period_days=retention_days,
)
await self.storage.save_consent(record)
await self.audit_logger.log_consent_request(record)
return record
async def grant_consent(
self,
consent_id: str,
evidence_type: str,
evidence_reference: str,
ip_address: str,
user_agent: str,
) -> ConsentRecord:
"""Record consent grant with evidence"""
record = await self.storage.get_consent(consent_id)
if not record:
raise ValueError("Consent record not found")
record.status = ConsentStatus.GRANTED
record.granted_at = datetime.utcnow()
record.expires_at = datetime.utcnow() + timedelta(
days=record.retention_period_days
)
record.evidence_type = evidence_type
record.evidence_reference = evidence_reference
record.ip_address = ip_address
record.user_agent = user_agent
await self.storage.save_consent(record)
await self.audit_logger.log_consent_grant(record)
return record
async def withdraw_consent(
self,
consent_id: str,
reason: str = None,
) -> ConsentRecord:
"""Process consent withdrawal (PDPL requirement)"""
record = await self.storage.get_consent(consent_id)
if not record:
raise ValueError("Consent record not found")
record.status = ConsentStatus.WITHDRAWN
record.withdrawn_at = datetime.utcnow()
await self.storage.save_consent(record)
await self.audit_logger.log_consent_withdrawal(record, reason)
# Trigger downstream actions (stop processing, schedule deletion)
await self._handle_withdrawal_consequences(record)
return record
async def check_consent(
self,
subject_id: str,
purpose: ConsentPurpose,
) -> bool:
"""Check if valid consent exists for processing"""
records = await self.storage.get_consents_for_subject(
subject_id, purpose
)
return any(r.is_valid() for r in records)
async def _handle_withdrawal_consequences(self, record: ConsentRecord):
"""Handle consequences of consent withdrawal"""
# Notify relevant systems to stop processing
await self.event_bus.publish("consent.withdrawn", {
"subject_id": record.subject_id,
"purpose": record.purpose.value,
"withdrawn_at": record.withdrawn_at.isoformat(),
})
# Schedule data deletion if no other legal basis
if not await self._has_other_legal_basis(record):
await self.deletion_scheduler.schedule(
subject_id=record.subject_id,
data_categories=record.data_categories,
reason="consent_withdrawn",
)
Consent UI Components
Copy
// React component for PDPL-compliant consent collection
interface ConsentFormProps {
purposes: ConsentPurpose[];
onConsent: (consents: ConsentGrant[]) => void;
language: 'ar' | 'en';
}
const ConsentForm: React.FC<ConsentFormProps> = ({
purposes,
onConsent,
language
}) => {
const [consents, setConsents] = useState<Record<string, boolean>>({});
const consentTexts = {
ar: {
treatment: 'أوافق على معالجة بياناتي الصحية لغرض العلاج الطبي',
research: 'أوافق على استخدام بياناتي المجهولة لأغراض البحث العلمي',
marketing: 'أوافق على تلقي اتصالات تسويقية',
crossBorder: 'أوافق على نقل بياناتي خارج المملكة العربية السعودية',
},
en: {
treatment: 'I consent to processing my health data for medical treatment',
research: 'I consent to use of my anonymized data for research purposes',
marketing: 'I consent to receiving marketing communications',
crossBorder: 'I consent to transfer of my data outside Saudi Arabia',
}
};
const handleSubmit = () => {
const grants = Object.entries(consents)
.filter(([_, granted]) => granted)
.map(([purpose, _]) => ({
purpose,
grantedAt: new Date().toISOString(),
consentTextVersion: hashConsentText(consentTexts[language][purpose]),
}));
onConsent(grants);
};
return (
<form onSubmit={handleSubmit} dir={language === 'ar' ? 'rtl' : 'ltr'}>
<h2>{language === 'ar' ? 'الموافقة على معالجة البيانات' : 'Data Processing Consent'}</h2>
{purposes.map(purpose => (
<div key={purpose} className="consent-item">
<input
type="checkbox"
id={purpose}
checked={consents[purpose] || false}
onChange={(e) => setConsents({
...consents,
[purpose]: e.target.checked
})}
/>
<label htmlFor={purpose}>
{consentTexts[language][purpose]}
</label>
{/* Required indicator for mandatory consents */}
{purpose === 'treatment' && (
<span className="required">
{language === 'ar' ? 'مطلوب' : 'Required'}
</span>
)}
</div>
))}
<div className="privacy-notice">
<a href="/privacy-policy">
{language === 'ar' ? 'سياسة الخصوصية' : 'Privacy Policy'}
</a>
</div>
<button type="submit">
{language === 'ar' ? 'تأكيد الموافقة' : 'Confirm Consent'}
</button>
</form>
);
};
Data Localization
PDPL Data Residency Requirements
Copy
class DataLocalizationManager:
"""
Manage PDPL data localization requirements
PDPL requires personal data to be stored within Saudi Arabia,
with limited exceptions for cross-border transfer.
"""
# Countries with adequate protection (as determined by SDAIA)
ADEQUATE_COUNTRIES = [
# To be updated as SDAIA issues adequacy decisions
# Similar to GDPR adequacy decisions
]
# Legal bases for cross-border transfer
TRANSFER_BASES = [
"explicit_consent",
"contract_performance",
"legal_obligation",
"vital_interests",
"public_interest",
"legal_claims",
]
def __init__(self, primary_region: str = "me-south-1"): # AWS Bahrain
self.primary_region = primary_region
async def can_transfer(
self,
destination_country: str,
data_category: str,
legal_basis: str,
has_consent: bool = False,
) -> dict:
"""Check if cross-border transfer is permitted"""
result = {
"permitted": False,
"requirements": [],
"recommendations": [],
}
# Check adequacy
if destination_country in self.ADEQUATE_COUNTRIES:
result["permitted"] = True
result["requirements"].append("Document adequacy decision reference")
return result
# Check legal basis
if legal_basis not in self.TRANSFER_BASES:
result["requirements"].append(f"Valid legal basis required. Options: {self.TRANSFER_BASES}")
return result
# Explicit consent for sensitive data
if data_category == "health_data" and not has_consent:
result["requirements"].append("Explicit consent required for health data transfer")
return result
# Standard contractual clauses
result["requirements"].extend([
"Execute Standard Contractual Clauses with recipient",
"Conduct Transfer Impact Assessment",
"Implement supplementary measures if needed",
])
result["permitted"] = True
return result
async def get_storage_location(self, data_category: str) -> dict:
"""Get appropriate storage location for data category"""
if data_category in ["health_data", "sensitive_data"]:
return {
"region": self.primary_region,
"cloud_provider": "aws",
"compliance": ["PDPL", "data_localization"],
"encryption": "required",
}
return {
"region": self.primary_region, # Default to Saudi/GCC region
"cloud_provider": "aws",
"compliance": ["PDPL"],
"encryption": "recommended",
}
# AWS/GCP region configuration for Saudi compliance
COMPLIANT_REGIONS = {
"aws": {
"primary": "me-south-1", # Bahrain (closest to Saudi)
"backup": "me-central-1", # UAE
},
"gcp": {
"primary": "me-central2", # Dammam, Saudi Arabia
"backup": "me-central1", # Doha
},
"azure": {
"primary": "uae-north", # Dubai
"backup": "uae-central",
}
}
Data Subject Rights
Rights Implementation
Copy
class DataSubjectRightsManager:
"""
Implement PDPL data subject rights
Rights include:
- Right to be informed
- Right of access
- Right to rectification
- Right to erasure
- Right to restrict processing
- Right to data portability
- Right to object
- Rights related to automated decision-making
"""
def __init__(self, storage, audit_logger, notification_service):
self.storage = storage
self.audit_logger = audit_logger
self.notifications = notification_service
async def handle_access_request(
self,
subject_id: str,
request_details: dict,
) -> dict:
"""
Handle data subject access request
PDPL: Must respond within 30 days
"""
request_id = str(uuid.uuid4())
# Log the request
await self.audit_logger.log({
"event_type": "DSR_ACCESS_REQUEST",
"subject_id": subject_id,
"request_id": request_id,
})
# Verify identity before proceeding
if not await self._verify_identity(subject_id, request_details):
return {
"status": "identity_verification_required",
"request_id": request_id,
}
# Collect all data about the subject
data = await self._collect_subject_data(subject_id)
# Generate portable format
export = {
"request_id": request_id,
"subject_id": subject_id,
"generated_at": datetime.utcnow().isoformat(),
"data_categories": list(data.keys()),
"data": data,
}
# Store for secure download
download_url = await self._create_secure_download(export)
# Notify subject
await self.notifications.send(
subject_id,
"data_access_ready",
{"download_url": download_url, "expires_in": "7 days"}
)
return {
"status": "completed",
"request_id": request_id,
"download_url": download_url,
}
async def handle_erasure_request(
self,
subject_id: str,
request_details: dict,
) -> dict:
"""
Handle right to erasure (right to be forgotten)
Must balance with record retention requirements
"""
request_id = str(uuid.uuid4())
# Check for legal retention requirements
retention_holds = await self._check_retention_requirements(subject_id)
if retention_holds:
return {
"status": "partially_completed",
"request_id": request_id,
"message": "Some data retained due to legal requirements",
"retained_categories": [h["category"] for h in retention_holds],
"retention_reasons": [h["reason"] for h in retention_holds],
}
# Collect all data locations
data_locations = await self._find_all_data(subject_id)
# Delete from each location
deletion_results = []
for location in data_locations:
result = await self._delete_from_location(location, subject_id)
deletion_results.append(result)
# Verify deletion
remaining = await self._verify_deletion(subject_id)
# Log completion
await self.audit_logger.log({
"event_type": "DSR_ERASURE_COMPLETED",
"subject_id": subject_id,
"request_id": request_id,
"locations_processed": len(data_locations),
"remaining_records": len(remaining),
})
return {
"status": "completed",
"request_id": request_id,
"deleted_from": len(data_locations),
}
async def handle_portability_request(
self,
subject_id: str,
format: str = "json",
destination: str = None,
) -> dict:
"""
Handle data portability request
Provide data in machine-readable format
"""
# Collect portable data
data = await self._collect_subject_data(subject_id)
# Convert to requested format
if format == "json":
export_data = json.dumps(data, indent=2, ensure_ascii=False)
elif format == "csv":
export_data = self._convert_to_csv(data)
elif format == "xml":
export_data = self._convert_to_xml(data)
else:
raise ValueError(f"Unsupported format: {format}")
# If destination provided, transfer directly
if destination:
await self._transfer_to_destination(destination, export_data)
return {"status": "transferred", "destination": destination}
# Otherwise, provide download
download_url = await self._create_secure_download(export_data)
return {"status": "ready", "download_url": download_url}
async def _check_retention_requirements(
self,
subject_id: str
) -> List[dict]:
"""Check for legal/regulatory retention requirements"""
holds = []
# HIPAA: Medical records retention
medical_records = await self.storage.get_medical_records(subject_id)
if medical_records:
holds.append({
"category": "medical_records",
"reason": "HIPAA 6-year retention requirement",
"retention_until": self._calculate_retention_date(medical_records),
})
# Financial records
financial_records = await self.storage.get_financial_records(subject_id)
if financial_records:
holds.append({
"category": "financial_records",
"reason": "Tax/accounting retention requirements",
"retention_until": self._calculate_retention_date(financial_records),
})
return holds
Breach Notification
PDPL Breach Requirements
Copy
class PDPLBreachManager:
"""
Manage breach notification per PDPL requirements
PDPL requires:
- Notify SDAIA within 72 hours
- Notify affected individuals without undue delay
- Maintain breach register
"""
NOTIFICATION_DEADLINE_HOURS = 72
def __init__(self, authority_api, notification_service, audit_logger):
self.authority_api = authority_api
self.notifications = notification_service
self.audit_logger = audit_logger
async def report_breach(self, breach_details: dict) -> dict:
"""Report a data breach"""
breach_id = str(uuid.uuid4())
discovery_time = datetime.utcnow()
deadline = discovery_time + timedelta(hours=self.NOTIFICATION_DEADLINE_HOURS)
breach_record = {
"breach_id": breach_id,
"discovered_at": discovery_time.isoformat(),
"notification_deadline": deadline.isoformat(),
"status": "investigating",
**breach_details,
}
# Store breach record
await self.storage.save_breach(breach_record)
# Assess severity
severity = await self._assess_severity(breach_details)
if severity in ["high", "critical"]:
# Immediate internal escalation
await self._escalate_internally(breach_record)
return {
"breach_id": breach_id,
"deadline": deadline.isoformat(),
"severity": severity,
"next_steps": self._get_next_steps(severity),
}
async def notify_authority(self, breach_id: str) -> dict:
"""
Notify SDAIA (Saudi Data & AI Authority)
Must be done within 72 hours of discovery
"""
breach = await self.storage.get_breach(breach_id)
notification = {
"breach_id": breach_id,
"organization": self.organization_details,
"discovery_date": breach["discovered_at"],
"nature_of_breach": breach["description"],
"data_categories_affected": breach["data_categories"],
"approximate_subjects_affected": breach["affected_count"],
"likely_consequences": breach["impact_assessment"],
"measures_taken": breach["remediation_steps"],
"contact_details": self.dpo_contact,
}
# Submit to SDAIA
response = await self.authority_api.submit_breach_notification(notification)
# Update breach record
breach["authority_notified_at"] = datetime.utcnow().isoformat()
breach["authority_reference"] = response["reference_number"]
await self.storage.save_breach(breach)
return response
async def notify_affected_subjects(self, breach_id: str) -> dict:
"""Notify affected data subjects"""
breach = await self.storage.get_breach(breach_id)
affected_subjects = await self._get_affected_subjects(breach_id)
notification_content = {
"ar": {
"subject": "إشعار بحادث أمني",
"body": self._generate_notification_ar(breach),
},
"en": {
"subject": "Security Incident Notification",
"body": self._generate_notification_en(breach),
}
}
results = {
"total": len(affected_subjects),
"notified": 0,
"failed": 0,
}
for subject in affected_subjects:
try:
await self.notifications.send_breach_notification(
subject["id"],
notification_content[subject.get("language", "en")],
breach_id,
)
results["notified"] += 1
except Exception as e:
results["failed"] += 1
await self.audit_logger.log_notification_failure(
breach_id, subject["id"], str(e)
)
return results
def _generate_notification_ar(self, breach: dict) -> str:
"""Generate Arabic breach notification"""
return f"""
عزيزي/عزيزتي،
نود إبلاغكم بوقوع حادث أمني قد يكون أثر على بياناتكم الشخصية.
ماذا حدث:
{breach['description']}
ما هي البيانات المتأثرة:
{', '.join(breach['data_categories'])}
ماذا نفعل:
{breach['remediation_steps']}
ماذا يمكنكم فعله:
- مراقبة حساباتكم للنشاط المشبوه
- تغيير كلمات المرور الخاصة بكم
- الاتصال بنا في حال لاحظتم أي نشاط غير عادي
للتواصل:
{self.contact_details}
"""
def _generate_notification_en(self, breach: dict) -> str:
"""Generate English breach notification"""
return f"""
Dear Valued Customer,
We are writing to inform you of a security incident that may have affected your personal data.
What Happened:
{breach['description']}
What Data Was Affected:
{', '.join(breach['data_categories'])}
What We Are Doing:
{breach['remediation_steps']}
What You Can Do:
- Monitor your accounts for suspicious activity
- Change your passwords
- Contact us if you notice any unusual activity
Contact Information:
{self.contact_details}
"""
Compliance Mapping
HIPAA to PDPL Control Mapping
Copy
COMPLIANCE_MAPPING = {
"access_control": {
"hipaa": "§164.312(a) - Access Control",
"pdpl": "Article 14 - Security of Personal Data",
"controls": [
"unique_user_identification",
"automatic_logoff",
"encryption",
"mfa",
]
},
"audit_controls": {
"hipaa": "§164.312(b) - Audit Controls",
"pdpl": "Article 14 - Processing Records",
"controls": [
"audit_logging",
"log_retention",
"log_review",
]
},
"integrity": {
"hipaa": "§164.312(c) - Integrity",
"pdpl": "Article 8 - Data Accuracy",
"controls": [
"data_validation",
"checksums",
"version_control",
]
},
"transmission_security": {
"hipaa": "§164.312(e) - Transmission Security",
"pdpl": "Article 14 - Security Measures",
"controls": [
"tls_encryption",
"vpn",
"secure_protocols",
]
},
"breach_notification": {
"hipaa": "§164.404 - Notification to Individuals",
"pdpl": "Article 20 - Personal Data Breach Notification",
"controls": [
"breach_detection",
"notification_within_72h",
"breach_documentation",
]
},
}
Key Takeaways
Consent is King
PDPL requires explicit consent for most processing, unlike HIPAA’s TPO exceptions
Data Must Stay Local
Keep personal data in Saudi Arabia/GCC unless you have valid legal basis for transfer
72-Hour Notification
Breach notification to SDAIA must occur within 72 hours of discovery
Rights Implementation
Build systems to handle access, erasure, and portability requests
Practice Exercise
1
Consent Flow
Design a consent collection flow for a healthcare app targeting Saudi users.
2
Data Mapping
Map your data flows and identify where PDPL data localization applies.
3
Rights Portal
Build a data subject rights portal supporting Arabic and English.
4
Breach Playbook
Create a breach response playbook meeting the 72-hour notification requirement.