HealthKit and Health Connect
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.
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 and an observation code. Use the platform's identifier values
and a code that matches the value and unit you intend to store.
import { MeasurementTypeKey, ObservationCode } from "@ovok/core";
import { HKQuantityTypeIdentifier } from "@kingstinct/react-native-healthkit";
import type { AppleHealthSyncProps } from "@ovok/native/data-sync";
export const appleDataToSync: AppleHealthSyncProps["dataToSync"] = {
[MeasurementTypeKey.bodyWeight]: {
typeIdentifiers: [
{
typeIdentifier: HKQuantityTypeIdentifier.bodyMass,
code: ObservationCode.BODY_WEIGHT,
},
],
minDate: new Date("2025-01-01T00:00:00.000Z"),
},
};
Android uses Health Connect record names for the same mapping:
import { MeasurementTypeKey, ObservationCode } from "@ovok/core";
import type { AndroidHealthSyncProps } from "@ovok/native/data-sync";
export const androidDataToSync: AndroidHealthSyncProps["dataToSync"] = {
[MeasurementTypeKey.bodyWeight]: {
typeIdentifiers: [
{ typeIdentifier: "Weight", code: ObservationCode.BODY_WEIGHT },
],
minDate: new Date("2025-01-01T00:00:00.000Z"),
},
};
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 latest day so a completed period can be
updated. The SDK also keeps provider pagination anchors; anchors persist across app
restarts when the OvokClient has storage configured.
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, and exercise minutes. |
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. |
The exported HealthDataType enum includes height, bodyFatPercentage,
bodyMassIndex, leanBodyMass, distance, activeEnergy,
flightsClimbed, exerciseMinutes, heartRateSummary, sleep, and
workout. 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";
import { HKQuantityTypeIdentifier } from "@kingstinct/react-native-healthkit";
export function AppleHealthImport() {
return (
<DataSync.AppleHealthAuthorizationProvider
readIdentifiers={[HKQuantityTypeIdentifier.bodyMass]}
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 client and patient from @ovok/core. Mount
them only after the user's session and patient profile are ready. 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.
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.