Health data that cannot leave the country
UAE health-data residency, after the incumbent storage vendor turned out to be unable to provide it and the backup plan went offline.
- Client
- Jabal Al Noor Pharmacy
- Industry
- Pharmacy retail
- Outcome
- Prescriptions resident in UAE North
The legal constraint
Prescriptions are health data generated in the UAE. Two regimes apply:
- Federal Law No. 2 of 2019 (ICT in the Health Sector) plus MOHAP Ministerial Decision 51/2021 — health data may not be stored or processed outside the UAE unless one of ten narrow exceptions applies.
- UAE PDPL (Federal Decree-Law No. 45 of 2021) — a general privacy regime, with fines up to AED 5 million.
The vendor problem
Cloudflare R2 hosts everything else on this project and cannot do this. Verified against R2 documentation rather than assumed:
| Feature | Coverage | Useful here |
|---|---|---|
| Location hints | wnam, enam, weur, eeur, apac, oc | No Middle East option |
| Jurisdictions | eu, fedramp only | The only real residency guarantee, and neither is the UAE |
| Regional Services | Mentions the UAE | Governs decryption in transit, not where objects rest |
Then the backup plan went offline
The plan was AWS S3 me-central-1. By build time both AWS UAE regions were down indefinitely with no recovery timeline on the AWS status dashboard. The build pivoted to Azure Blob Storage in UAE North — same residency guarantee, different vendor.
The original AWS reasoning is kept struck through in the design doc rather than deleted, so the history of why we did not end up on AWS survives.
What shipped
Storage
Prescriptions live in a private Azure container with no CDN in front. The database stores the raw blob name rather than a URL, precisely so nothing can ever be served unsigned, and every read mints a fresh ten-minute read-only per-blob SAS. A keyFromUrl() fallback handles legacy rows from when prescriptions briefly lived on R2.
Product images and delivery-proof photos stay on R2: non-health, no residency constraint, no reason to pay for the stricter path.
Consent
Captured as append-only records rather than a boolean on the user row.
- Registration requires
healthDataConsent: z.literal(true)— Zod rejects the request if unchecked - Marketing consent defaults to
false, with no pre-ticked box - Both grants and declines are recorded, so the trail proves what was actually offered
- Versioned by a
CONSENT_POLICY_VERSIONconstant, so old rows stay attributable to the text the user really saw - Prescription upload re-confirms consent per upload, because a fresh prescription is fresh health data
Data-subject rights
Shipped as self-service: a JSON export of profile, addresses, orders with line-item snapshots, prescription metadata, reviews and full consent history. Prescription file URLs are deliberately omitted, since a signed URL would expire minutes later.
Deletion is a reviewed workflow rather than an immediate erase, and erasure anonymises in place. The email becomes a unique erased+{id}@ placeholder rather than NULL, to avoid colliding with the unique constraint on repeat erasures, while orders and prescriptions are retained to avoid orphaning rows with accounting and regulatory retention needs.
The honest part
The design doc records this as provisional engineering, not a legal determination. Prescription retention minimums under UAE pharmacy law are unresolved, and no UAE healthcare-data lawyer has reviewed whether anonymise-in-place satisfies PDPL erasure for this data mix.
That is flagged for counsel rather than quietly assumed. Engineering flags exposure; counsel rules on classification.