DEV Community

Cover image for Building a Zero-Cloud Medical Data Pipeline on Mobile (Apple HealthKit & Health Connect)
Alessandro Mainardi
Alessandro Mainardi

Posted on Originally published at alexmaina.dev

Building a Zero-Cloud Medical Data Pipeline on Mobile (Apple HealthKit & Health Connect)

πŸ“– Original Reference: Originally published with full architecture diagrams and cryptographic proofs at alexmaina.dev/Health-App. Technical monograph published on Zenodo (DOI: 10.5281/zenodo.23735226).

Most health apps ask you to upload blood test reports, doctor notes, and medical records to their backend servers for "AI processing" and parsing. For sensitive biometric and clinical data, this architecture introduces catastrophic compliance risks under GDPR Art. 9, HIPAA, and patient privacy expectations.

When building RosettaEdge β€” an open-source clinical bridge for iOS and Android β€” we enforced an uncompromising constraint: Zero-Cloud Architecture. 100% of data parsing, OCR extraction, database indexing, and health store synchronization happens entirely on the user's silicon.

Here is how the on-device medical ingestion pipeline works, and how you can implement local health data ingestion in Swift and Kotlin.


1. The Ingestion Pipeline Architecture

A clinical document usually arrives as an unstructured PDF (scanned or digitally exported from a hospital portal) or an XML file compliant with HL7 CDA Release 2 (Clinical Document Architecture).

The local pipeline executes in 4 strictly isolated phases:

[ Clinical PDF / CDA2 XML ]
             ↓ (1)
[ Local Memory Buffer (Secure Enclave / Keystore Key) ]
             ↓ (2)
[ Regex Tokenizer & LOINC / UCUM Normalizer ]
             ↓ (3)
[ Encrypted Local Vault (SQLCipher AES-256-GCM) ]
             ↓ (4)
[ HealthKit (iOS) / Health Connect (Android) Batch Sync ]
Enter fullscreen mode Exit fullscreen mode

2. Parsing Clinical PDFs Locally without External APIs

Instead of sending raw PDFs to cloud OCR APIs, modern mobile chips (Apple Neural Engine and Android ML Kit) can extract tabular clinical observations in sub-second execution times.

Here is a simplified pattern in Swift using PDFKit to extract lab parameters into structured clinical models:

import Foundation
import PDFKit

public struct ClinicalObservation {
    public let parameterName: String
    public let value: Double
    public let unit: String
    public let referenceRange: ClosedRange<Double>?
    public let loincCode: String?
}

public final class LocalLabExtractor {
    public static func parse(pdfDocument: PDFDocument) -> [ClinicalObservation] {
        var observations: [ClinicalObservation] = []
        guard let pageCount = Optional(pdfDocument.pageCount), pageCount > 0 else { return [] }

        // Fast-path text extraction directly from PDF vector stream
        for pageIndex in 0..<pageCount {
            guard let page = pdfDocument.page(at: pageIndex),
                  let rawText = page.string else { continue }

            // Regex matching: Parameter Name | Value | Unit | Reference Interval
            // Example match: "Emoglobina (Hb)  14.2 g/dL [13.0 - 17.5]"
            let pattern = #"([A-Za-zΓ€-ΓΏ\s\(\)]+)\s+([0-9]+[.,]?[0-9]*)\s+([a-zA-Z\/%]+)\s*(\[[0-9.,\s-]+\])?"#
            if let regex = try? NSRegularExpression(pattern: pattern, options: []) {
                let nsString = rawText as NSString
                let matches = regex.matches(in: rawText, options: [], range: NSRange(location: 0, length: nsString.length))

                for match in matches {
                    let name = nsString.substring(with: match.range(at: 1)).trimmingCharacters(in: .whitespacesAndNewlines)
                    let rawVal = nsString.substring(with: match.range(at: 2)).replacingOccurrences(of: ",", with: ".")
                    let unit = nsString.substring(with: match.range(at: 3)).trimmingCharacters(in: .whitespacesAndNewlines)

                    if let doubleVal = Double(rawVal) {
                        observations.append(ClinicalObservation(
                            parameterName: name,
                            value: doubleVal,
                            unit: unit,
                            referenceRange: nil,
                            loincCode: nil
                        ))
                    }
                }
            }
        }
        return observations
    }
}
Enter fullscreen mode Exit fullscreen mode

3. Standardizing to LOINC and UCUM

Hospitals use dozens of informal variations for the same test (e.g., "Glicemia", "Glucosio basale", "Fasting Glucose"). Apple HealthKit and Google Health Connect require standardized medical identifiers:

  • LOINC (Logical Observation Identifiers Names and Codes): e.g., 2345-7 for Glucose in blood.
  • UCUM (Unified Code for Units of Measure): e.g., mg/dL or mmol/L.

Map your parsed names to LOINC codes via an embedded, pre-compiled lookup dictionary to guarantee zero network latency:

let loincRegistry: [String: (loinc: String, hkType: HKQuantityTypeIdentifier)] = [
    "glucosio": ("2345-7", .bloodGlucose),
    "glicemia": ("2345-7", .bloodGlucose),
    "emoglobina": ("718-7", .bloodPressureSystolic) // Map to matching HealthKit identifier
]
Enter fullscreen mode Exit fullscreen mode

4. Writing into HealthKit with Idempotent Deduplication

The most common bug when importing health documents is duplicate samples: if the user imports the same laboratory report twice, HealthKit will chart double entries.

Always attach an idempotent metadata key (HKMetadataKeyExternalUUID) derived from the SHA-256 hash of (TestDate + LOINC + Value):

import HealthKit
import CryptoKit

func saveToHealthKit(store: HKHealthStore, obs: ClinicalObservation, sampleDate: Date) async throws {
    guard let quantityType = HKQuantityType.quantityType(forIdentifier: .bloodGlucose) else { return }

    // Generate deterministic hash for deduplication
    let hashPayload = "\(obs.parameterName)_\(obs.value)_\(sampleDate.timeIntervalSince1970)"
    let externalId = SHA256.hash(data: Data(hashPayload.utf8)).compactMap { String(format: "%02x", $0) }.joined()

    let quantity = HKQuantity(unit: HKUnit(from: "mg/dL"), doubleValue: obs.value)
    let sample = HKQuantitySample(
        type: quantityType,
        quantity: quantity,
        start: sampleDate,
        end: sampleDate,
        metadata: [
            HKMetadataKeyExternalUUID: externalId,
            HKMetadataKeySyncVersion: 1
        ]
    )

    try await store.save(sample)
}
Enter fullscreen mode Exit fullscreen mode

5. Security & Zero-Cloud Guarantees

To ensure complete patient privacy:

  1. No External Analytics SDKs: Strip Firebase, Mixpanel, and third-party trackers.
  2. Encrypted Storage: Index document metadata in a local SQLite database encrypted with SQLCipher (AES-256-GCM), with the master key stored in the platform's hardware key store (iOS Secure Enclave / Android Keystore).
  3. Biometric Vault: Protect the database access gate with Face ID, Touch ID, or BiometricPrompt.

Conclusion

Mobile operating systems now have sufficient hardware performance to process, parse, and analyze unstructured medical documents without depending on remote servers. Designing for zero-cloud architecture is not just better for privacy β€” it also eliminates server infrastructure costs and guarantees instant offline functionality.

Author: Alessandro Mainardi β€” Computer Engineering Student @ UniBo & Developer of RosettaEdge. Read the 44-page technical monograph at alexmaina.dev/Health-App or on Zenodo.

Top comments (0)