HealthKit and Health Connect
Configure permissions and import device health data with the OVOK SDK
- Stable unknown
- Release candidate since 1.16.1-rc.3
- Development since 1.16.1-dev.2111+9796905
Use the data sync module to import selected records from Apple HealthKit on iOS or Health Connect on Android. The authorization provider handles the platform permission flow; its matching sync component reads records and saves them as OVOK observations for the signed-in patient.
Apple Health import needs @kingstinct/react-native-healthkit 16.2.0 or newer and
react-native-nitro-modules 0.35 or newer. Both are native modules: after you add or
upgrade them, build the app again, because a JavaScript update cannot load them.
HealthKit identifiers are plain strings such as "HKQuantityTypeIdentifierBodyMass";
version 16 has no HKQuantityTypeIdentifier enum to import.
Before adding the integration, complete installation and native setup and mount the app inside the authenticated OVOK provider tree. Health sync needs an initialized client and a patient profile, and it runs in a native development build rather than Expo Go.
Pick the platform API
| Platform | Authorization provider | Import component | Native source |
|---|---|---|---|
| iOS | DataSync.AppleHealthAuthorizationProvider | DataSync.AppleHealthSync | HealthKit |
| Android | DataSync.AndroidHealthConnectAuthorizationProvider | DataSync.AndroidHealthSync | Health Connect |
Import the module from @ovok/native/data-sync. The optional, adapter-driven
HealthImportCard is a separate UI primitive;
it does not replace these providers or run this import pipeline.
1. Map records to OVOK observations
Each dataToSync entry connects an OVOK measurement key to one or more platform
record identifiers. Leave code out: the SDK codes a core measurement key the way
core codes that measurement from any source, and any other key by its reviewed
LOINC term or the platform's own type. Set code and system only to store a
type under a different code.
import { MeasurementTypeKey } from "@ovok/core/next";
import type { AppleHealthSyncProps } from "@ovok/native/data-sync";
export const appleDataToSync: AppleHealthSyncProps["dataToSync"] = {
[MeasurementTypeKey.bodyWeight]: {
typeIdentifiers: [
{ typeIdentifier: "HKQuantityTypeIdentifierBodyMass" },
],
minDate: new Date("2025-01-01T00:00:00.000Z"),
},
};Android uses Health Connect record names for the same mapping:
import { MeasurementTypeKey } from "@ovok/core/next";
import type { AndroidHealthSyncProps } from "@ovok/native/data-sync";
export const androidDataToSync: AndroidHealthSyncProps["dataToSync"] = {
[MeasurementTypeKey.bodyWeight]: {
typeIdentifiers: [
{ typeIdentifier: "Weight" },
],
minDate: new Date("2025-01-01T00:00:00.000Z"),
},
};Apple Watch ECG
Map MeasurementTypeKey.ecg to the HKElectrocardiogramType identifier to import
the recordings made with the Apple Watch ECG app (iOS 14 or later):
[MeasurementTypeKey.ecg]: {
typeIdentifiers: [{ typeIdentifier: "HKElectrocardiogramType" }],
},Each recording becomes OVOK's ECG observation: the waveform in millivolts as sampled
data, its duration in seconds, and the average heart rate as a second observation. The
start of the recording is its time. Apple's own classification (such as sinus rhythm
or atrial fibrillation) and the symptoms the person noted are not imported: the SDK
saves what was measured and gives no reading of it. Add HKElectrocardiogramType to
readIdentifiers of the authorization provider as well. Recordings are read at most
ten at a time, whatever chunkSize is, because each one holds the whole waveform.
The first read defaults to the most recent 30 days on both platforms. Set
minDate to choose an earlier initial boundary. Later runs use the latest matching
observation already stored for the patient and provider as their starting point.
Daily totals and summaries re-read the last seven imported days, so a total that a
wearable changes after a late sync is updated. The SDK also keeps provider pagination
anchors in the native client's secure storage, scoped to the current tenant,
project, patient, provider, and data type.
When a record is deleted in Apple Health or Health Connect, the observations imported
from it are deleted too. They are found by the observation-sync-record identifier
the import gives them; observations imported before it existed don't have it, so
they stay.
The SDK has built-in modes for common data shapes:
mode | Result |
|---|---|
point | One observation per source sample or record. This is the default for most measurement keys. |
daily-total | One total for each completed day. This is the default for step count, distance, active energy, flights climbed, exercise minutes, water, carbohydrates, and calories eaten. |
daily-summary | One daily summary. Heart rate defaults to an average with minimum and maximum components. |
session | One observation for each session. Sleep and workout default to this mode. |
series | One observation per finished hour holding a value a minute as valueSampledData (E for a minute without one), for frequent quantities such as heart rate, SpO₂ and respiratory rate. Apple Health's statistics and Health Connect's aggregation give the minute averages, following the user's source priority; Health Connect types it doesn't aggregate (SpO₂, respiratory rate) are averaged from their records. Each run reads the last three imported hours again and writes over them. |
The exported HealthDataType enum includes height, bodyFatPercentage,
bodyMassIndex, leanBodyMass, distance, activeEnergy,
flightsClimbed, exerciseMinutes, heartRateSummary, sleep, workout,
water, carbohydrates, caloriesEaten, bloodAlcohol, and insulin.
Health Connect has no blood alcohol or insulin record. Use these keys for data that is not represented by a core measurement
key. Set mode on an entry to override its default.
2. Configure native permissions
Add the SDK config plugin for the platform integrations your app uses. For an
import-only flow, request only read permissions for the record types in
dataToSync:
plugins: [
[
"@ovok/native",
{
healthKit: true,
healthConnect: true,
},
],
],
ios: {
infoPlist: {
NSHealthShareUsageDescription:
"Explain why the app reads the selected health data.",
},
},
android: {
permissions: ["android.permission.health.READ_WEIGHT"],
},For HealthKit background delivery, use healthKit: { background: true } and add
the background delivery option in step 5. Add
NSHealthUpdateUsageDescription and HealthKit write permissions only if your app
also writes data back to the health store. For Health Connect, declare the matching
android.permission.health.READ_* permissions for every record type you request.
The plugin composes the platform libraries; native config changes take effect after
you rebuild the app.
See installation and native setup for plugin details, usage descriptions, and development builds.
3. Mount the authorization provider and sync component
The provider requests access automatically when a request is needed. Pass only the
identifiers the feature will read. For import-only flows, omit writeIdentifiers.
The mapping objects, status setters, progress state, and error handler below are
app-owned values; define them in the screen or feature module.
import * as React from "react";
import { ActivityIndicator } from "react-native";
import { DataSync } from "@ovok/native/data-sync";
export function AppleHealthImport() {
return (
<DataSync.AppleHealthAuthorizationProvider
readIdentifiers={["HKQuantityTypeIdentifierBodyMass"]}
fallback={<ActivityIndicator />}
onAuthorizationStatusChange={setHealthKitStatus}
>
<DataSync.AppleHealthSync
dataToSync={appleDataToSync}
onProgress={setProgress}
onError={handleSyncError}
/>
</DataSync.AppleHealthAuthorizationProvider>
);
}For Health Connect, use its record identifiers and provider:
import { ActivityIndicator } from "react-native";
import { DataSync } from "@ovok/native/data-sync";
export function HealthConnectImport() {
return (
<DataSync.AndroidHealthConnectAuthorizationProvider
readIdentifiers={["Weight"]}
fallback={<ActivityIndicator />}
onAuthorizationStatusChange={setHealthConnectStatus}
>
<DataSync.AndroidHealthSync
dataToSync={androidDataToSync}
onProgress={setProgress}
onError={handleSyncError}
/>
</DataSync.AndroidHealthConnectAuthorizationProvider>
);
}Both sync components read the active native client and Patient session from
@ovok/core/next/react. Mount them only after sign-in. The provider
controls the permission UI; the sync component starts its import when mounted.
Own the permission screen
Provide both renderManualRequestUI and skipRequest when your app owns the
prompt. Set skipRequest={false} to let the provider render the callback UI and
avoid automatic permission requests:
import { Button } from "react-native";
import { DataSync } from "@ovok/native/data-sync";
<DataSync.AndroidHealthConnectAuthorizationProvider
readIdentifiers={["Weight"]}
skipRequest={false}
renderManualRequestUI={(requestAccess) => (
<Button title="Continue" onPress={requestAccess} />
)}
>
<DataSync.AndroidHealthSync dataToSync={androidDataToSync} />
</DataSync.AndroidHealthConnectAuthorizationProvider>The callback receives the provider's requestAccess function. To build a bottom
sheet with the supplied pieces, see
ManuallyRequestSheet. A Skip
action is app behavior: update the screen or navigation and do not leave an import
component mounted behind a skipped permission prompt. Setting skipRequest={true}
bypasses the provider's request gate and renders its children.
Apple does not expose whether a person denied read access to a particular HealthKit
type. Its status reports types that still need a response, but a ready state is
not proof that every requested type is readable. Treat empty reads as a valid
outcome.
4. Render progress and handle errors
The sync components report progress as a partial map keyed by the configured
measurement key. Render it with the exported SyncProgressList or provide your own
UI:
import { SyncProgressList } from "@ovok/native/data-sync";
<SyncProgressList
progress={progress}
labelForType={(type) => measurementLabels[type] ?? type}
statusLabels={{ syncing: "Importing", completed: "Imported" }}
/>;SyncProgress includes measurementTypeKey, status, optional pause
reason, and optional progress.done / progress.total counts. Status values
are idle, started, syncing, paused, failed, and completed.
Pause reasons are offline and wifi-only.
chunkSize defaults to 250 records per provider read. wifiOnly defaults to
false; when the device is offline, the sync pauses, and when wifiOnly is true,
it also pauses on non-Wi-Fi networks. The sync resumes when the connection meets the
configured policy.
onError receives a Bundle<Resource>, an Error, or null. Narrow the
value before reading error-specific fields, and report failures through the app's
normal error handling.
Health saves retry HTTP 429, 500, 502, 503 and 504 responses up to three times. A refusal (400, 401, 403, 404,
409, 422) and a 501, a feature the environment does not have, fail at once. Each wait is the
greater of the normal 1, 4, and 16 second backoff and the server's Retry-After
hint. If all retries fail, the final error reaches onError.
5. Add background delivery only when needed
For HealthKit observer updates, pass backgroundDelivery to
AppleHealthSync and enable the HealthKit background capability in the config
plugin. Background delivery asks iOS to notify the app about new samples; the OS
controls when the app runs.
For Android Health Connect imports after the app process stops, use the registered
headless task and scheduler in the background sync guide.
Set backgroundAccess on the authorization provider to request Health Connect's
separate background-read permission. That permission is reported independently from
foreground access, and declining it does not block foreground imports.
Do not render AndroidHealthSync inside a headless task. The headless API runs an
import with an explicitly restored client, patient ID, and mapping.
Component references
- DataSync module and compound API
- Apple Health authorization provider
- Apple Health sync
- Android Health Connect authorization provider
- Android Health sync
- SyncProgressList
- ManuallyRequestSheet and its components
- HealthImportCard
For a separate scheduled Android import, continue with background sync.
API reference
Every export of @ovok/native/data-sync, with signatures, props and types, is in the data-sync API reference.
Last updated on