Skip to content

Wearable / Device Signals (PADRE M46)

Metricis passively ingests wearable/device signals — heart rate, steps, active minutes, sleep, blood pressure, and HRV — from the participant's phone health store and turns them into patient-understandable derived metrics. This is the objective, continuous layer of the PADRE measurement system (alongside PROMs and the daily diary).

Nothing here is clinical-grade: wrist PPG is affected by motion, cold, and tremor, so every metric is a research estimate, poor-quality samples are excluded, and signal quality / wear-time travel with the data.

Data flow

Native health store            Capacitor plugin        Sync service            Server
(HealthKit / Health Connect) → HealthBridge.query() → deviceSignalSync.ts → POST /submit/device-signal
                                                         (map + watermark)     → DeviceSignalSession
                                                                                 + DeviceSignalReading
                                                                                 + derived metrics
  • iOS reads Apple HealthKit.
  • Android reads Health Connect — the on-device successor to Google Fit, which also surfaces data written by Google Fit, Fitbit, Samsung Health, etc. (Google Fit's Android APIs are deprecated and shutting down; Health Connect is the correct target.)
  • Web has no health store: the plugin reports unavailable and passive sync no-ops. Manual entry is the web path.

Client pieces

File Role
packages/capacitor-health-bridge/ Shared workspace Capacitor plugin (@metricis/capacitor-health-bridge, since M76; client adopted M77): TS contract + web fallback in src/, iOS Swift (ios/SourcesCAPBridgedPlugin, HealthKit) and Android Kotlin (android/src — Health Connect) natives. Discovered by cap sync in any consuming app (npm-package plugins register via packageClassList — no app-local registration code).
client/src/services/health/mappers.ts Pure: native samples → DeviceSignalPayload, incremental sync windows, idempotency keys
client/src/services/health/deviceSignalSync.ts Orchestrator: availability → authorization → per-signal query → submit, with per-signal watermarks

The patient-portal has its own ports of the mappers/sync service (patient-portal/src/services/health/) consuming the same plugin — see Mobile and offline delivery.

Triggering a sync

syncDeviceSignals() is native-only, offline-aware, and idempotent (per-signal watermark + server dedup on clientSubmissionId). Call it opportunistically — e.g. on app resume or after an assessment — once the participant is known:

import { requestDeviceSignalPermissions, syncDeviceSignals } from './services/health';

await requestDeviceSignalPermissions(); // presents the platform permission sheet
const result = await syncDeviceSignals(participantCode);
// result.outcomes: per-signal { status: 'synced' | 'empty' | 'skipped' | 'error', sampleCount? }

It is intentionally not wired into the assessment timeline — when to sync is a product decision. On web / when offline / when unauthorized, it returns a skipped result rather than throwing.

Signal → native mapping

Signal HealthKit Health Connect Unit sent
heart_rate HKQuantityType.heartRate HeartRateRecord count/minbpm
steps stepCount StepsRecord count
active_minutes appleExerciseTime ExerciseSessionRecord (duration) min
sleep sleepAnalysis (category) SleepSessionRecord (stages) min (+ context stage)
blood_pressure systolic/diastolic quantities BloodPressureRecord mmHg (+ context systolic/diastolic)
hrv heartRateVariabilitySDNN HeartRateVariabilityRmssdRecord ms

Native unit strings are passed through verbatim; the server-side source adapter (device_signal_adapters.py) normalizes aliases to canonical units.

Manual native integration steps

The JS layer is verified by pnpm --filter @metricis/client run typecheck, lint, and build. The native code must be compiled in Xcode / Android Studio (not possible in a headless environment) and needs these one-time steps:

iOS

  1. In Xcode, select the App target → Signing & Capabilities+ CapabilityHealthKit (adds the entitlement + provisioning profile capability). The com.apple.developer.healthkit entitlement is already in App.entitlements; the usage strings are in Info.plist.
  2. Ensure the provisioning profile / App ID has HealthKit enabled in the Apple Developer portal.
  3. cd client && pnpm run build:ios then run on a real device (the simulator has no Health data).
  4. Localize NSHealthShareUsageDescription (fr) via InfoPlist.strings before release.

Android

  1. The Kotlin toolchain + androidx.health.connect:connect-client pins live in the plugin package's own packages/capacitor-health-bridge/android/build.gradle (self-contained buildscript; consuming apps need no Kotlin wiring). Bump healthConnectVersion there to the latest stable at integration time and confirm the Kotlin version matches AGP 8.13.
  2. On Android 13 and below, Health Connect is a separate app — the store must be installed for isAvailable() to return true. On Android 14+ it is part of the system.
  3. Complete Google Play's Health Connect / health data declaration and privacy-policy requirements before release (health permissions are restricted).
  4. cd client && pnpm run build:android then run on a device/emulator with Health Connect data.
  • Read-only: the plugin never writes to the health store.
  • The server enforces the consent gate (operation="device_data") on every upload and writes an audit entry with counts/quality only — never raw vitals.
  • Device model is stored as an accuracy covariate, not an identifier.