expo-unified-health
v0.1.0
Published
Expo SDK for Apple HealthKit and Android Health Connect — read, write, and delete health data.
Downloads
24
Maintainers
Readme
expo-unified-health
Expo SDK for Apple HealthKit and Android Health Connect — read, write, and delete health data with a unified, type-safe API.
Requirements
| Platform | Minimum | |---|---| | iOS | 16.0 (HealthKit) | | Android | API 34 + Health Connect installed | | Expo SDK | 50+ |
Installation
npx expo install expo-unified-healthAdd the config plugin to app.json (required — it sets the HealthKit entitlement on iOS and the Health Connect permissions on Android):
{
"expo": {
"plugins": ["expo-unified-health"]
}
}Then rebuild your dev client:
npx expo prebuild
npx expo run:ios # or run:androidQuick Start
import {
requestPermissions,
readRecords,
writeRecords,
deleteRecords,
} from "expo-unified-health";
// 1. Request permissions
const { granted, denied } = await requestPermissions({
read: ["steps", "heartRate", "sleep"],
write: ["steps", "weight"],
});
// 2. Read — result type is inferred from the literal "steps"
const { records } = await readRecords("steps", {
startDate: new Date(Date.now() - 86_400_000).toISOString(), // last 24 h
endDate: new Date().toISOString(),
});
// records: StepsRecord[] (fully typed, no cast needed)
console.log(records[0].count); // number
// 3. Write
const { ids } = await writeRecords("steps", [
{
startDate: "2026-01-01T08:00:00.000Z",
endDate: "2026-01-01T09:00:00.000Z",
count: 1000,
},
]);
// 4. Delete by ID or time range
await deleteRecords("steps", { ids });API
import {
getAvailability,
getCapabilities,
getSupportedRecordTypes,
readRecords,
writeRecords,
deleteRecords,
updateRecords,
requestPermissions,
getGrantedReadPermissions,
} from "expo-unified-health";requestPermissions(options)
const result = await requestPermissions({
read: ["steps", "heartRate", "sleep"],
write: ["steps", "weight"],
});
// result.granted — HealthRecordType[]
// result.denied — HealthRecordType[]
// result.statusByType — Partial<Record<HealthRecordType, "granted" | "denied" | "unknown">>getAvailability()
const { platform, status, canRequestPermissions } = await getAvailability();
// status: "available" | "unavailable" | "unsupported" | "notInstalled" | "notInitialized"getGrantedReadPermissions()
Returns the set of read permissions already granted without triggering a system prompt.
const { granted } = await getGrantedReadPermissions();readRecords(type, query)
const { records, nextPageToken, error } = await readRecords("heartRate", {
startDate: "2026-01-01T00:00:00.000Z",
endDate: "2026-01-02T00:00:00.000Z",
limit: 100, // optional
ascending: true, // optional
sourceIds: ["..."], // optional — filter by source app/device
});
// records: HeartRateRecord[]writeRecords(type, records)
const { ids } = await writeRecords("steps", [
{ startDate: "...", endDate: "...", count: 1000 },
]);
// ids: string[] — platform-assigned IDs for the written recordsdeleteRecords(type, query)
// By IDs
await deleteRecords("steps", { ids: ["id-1", "id-2"] });
// By time range
await deleteRecords("steps", {
startDate: "2026-01-01T00:00:00.000Z",
endDate: "2026-01-02T00:00:00.000Z",
});updateRecords(type, records)
Neither HealthKit nor Health Connect supports native in-place update. updateRecords is a JS convenience: it deletes the records by ID and re-inserts them with the new values.
await updateRecords("steps", [
{ id: "existing-id", startDate: "...", endDate: "...", count: 1200 },
]);getCapabilities(type)
const caps = getCapabilities("steps");
// {
// type, tier, read, write, aggregate, background,
// platformDetailsRequired, supportedPlatforms, notes?
// }getSupportedRecordTypes()
Returns all record types the current platform supports.
Record Types
Tier 1 — full read + write + delete
| Type | Write fields |
|---|---|
| steps | startDate, endDate, count |
| heartRate | startDate, endDate, beatsPerMinute |
| restingHeartRate | startDate, endDate, beatsPerMinute |
| distance | startDate, endDate, meters |
| activeCaloriesBurned | startDate, endDate, kilocalories |
| weight | startDate, endDate, kilograms |
| sleep | startDate, endDate |
| workout | startDate, endDate, activityType?, totalDistanceMeters?, totalEnergyKilocalories? |
Tier 2 — read only
bloodGlucose, bloodPressure, hrv, nutrition, respiratoryRate, vo2Max
Running the Example App
The example/ directory contains a working Expo dev client app with Maestro e2e tests for both platforms. See example/README.md for full setup instructions.
Android:
bun --cwd example android
# Between runs, reset Health Connect state:
bun android:reset-demoiOS:
bun --cwd example iosTroubleshooting
With Xcode 26 / Apple clang 21, the generated project can fail while compiling the React Native fmt pod before this module's Swift code is reached:
Pods/fmt/include/fmt/format-inl.h: call to consteval function ... is not a constant expressionThis is a toolchain issue, not an SDK issue. Patch the generated example/ios/Podfile post_install block after expo prebuild:
fmt_base_header = File.join(__dir__, 'Pods', 'fmt', 'include', 'fmt', 'base.h')
if File.exist?(fmt_base_header)
contents = File.read(fmt_base_header)
patched = contents.gsub('# define FMT_USE_CONSTEVAL 1', '# define FMT_USE_CONSTEVAL 0')
if patched != contents
File.chmod(0o644, fmt_base_header)
File.write(fmt_base_header, patched)
end
endThen reinstall pods and rebuild:
cd example/ios && pod install && cd ../..
bun --cwd example iosContributing
See CONTRIBUTING.md.
License
MIT — see LICENSE.
