Skip to content

@gj-kit/expo-workouts core concepts

HealthKit and Health Connect workouts in Expo, where destroying stored data is a compile error.

A Health Connect upsert is full-state: a saveWorkout that omits the route deletes the route already stored, and a write with a lower version returns normally with the same record id, so nothing but a read-back can tell it apart from a real write. HealthKit never reports read authorization at all, so a scope you forgot to declare leaves distanceM undefined on every workout with no error anywhere. And a sync loop that commits its cursor before the rows it just received loses those workouts permanently.

HealthKit and Health Connect require a development build and platform permission. Availability is exposed instead of pretending Expo Go or web can perform native work.

A locked device, denied permission, and a saved workout are distinct results. A native ID is available only after the saved branch is proven.

  • Route omission is a compile error — WorkoutWrite.route is a required readonly RoutePoint[] | 'none', so omitting it is TS2741 — a Health Connect upsert without it deletes the route already stored.
  • Locked-device branch cannot be skipped — SaveResult is a union discriminated on status, so saved.nativeId is TS2339 until status === 'saved' narrows away the pendingUnlock branch a locked device produces.
  • Missing scopes named before reading — unpopulatedWorkoutMetrics(state) returns the Workout field names, distanceM among them, whose gating read scope is denied or never requested — before you read a single workout.
  • Safe to import anywhere — The node/browser condition routes to index.unsupported, whose built module graph contains no expo; in Expo Go requireOptionalNativeModule returns null, so import never throws and getAvailability() resolves unavailable.
  • Sync gaps reproduce on Node — createFakeWorkouts() replaces the NativeWorkoutsModule seam rather than WorkoutsApi, so real ./core code runs under vitest for all six CursorResetReason values and a crash mid-drain.

This is a native module: Expo Go and web/Node are intentionally unsupported for native calls. Explain health permissions before requesting them.

Continue with the two verified recipes.