September 17, 2026 · 9 min read · Sugam Budhraja

Can a Web App Read Apple Health?

No. There is no HealthKit in Safari, no Apple Health REST API, and no OAuth a browser can call. Capacitor, Ionic and a WKWebView with a native plugin work because they are iOS apps with the HealthKit entitlement, which means App Review, purpose strings and a binary — not a PWA.

No.

There is no HealthKit in Safari, no Apple Health REST API, and no OAuth endpoint a browser can call for a user’s Health app [1]. A Next.js coaching product, a GLP-1 companion that lives on the web, and a PWA added to the home screen all sit on the same side of that line.

Capacitor does not move the line. It hides it. Official Capacitor health plugins document that they are not available on web and reject in the browser [2] [3]. They work on iOS because the project has become an iOS app: HealthKit capability, usage strings, App Store. The JavaScript you wrote is a UI. The entitlement is native.

This is the stack table, then the two architectures that actually ship. The broader consent map is four authorization models. What happens after you add HealthKit to that binary is App Store rejection.


What people try

StackCan it read HealthKit?Why
Browser JavaScript / SafariNoNo web API. HealthKit is an on-device framework [1]
PWA (standalone display)NoStill the browser’s security context. No entitlements
WKWebView, no native bridgeNoA WebView is not HKHealthStore
WKWebView + native message handlerYes, the native side canYou wrote an iOS app that happens to render HTML
Capacitor / Ionic health pluginYes, in a debug/device iOS build. No, on webPlugins ship native HealthKit code and an unimplemented web stub [2] [3]
Expo GoNoExpo Go is a prebuilt binary without your HealthKit entitlement
Expo with prebuild / a custom native moduleYesnpx expo prebuild (or a bare workflow) produces an iOS app. Same App Review as any other
React Native / Flutter / native SwiftYesNative binary, HealthKit entitlement
Backend cron against AppleNoThere is no token to redeem

The interesting rows are the middle ones, because they are how web teams talk themselves into “we shipped HealthKit” without booking App Review.

If npx cap sync is in the critical path, you are not arguing about the web platform. You are arguing about whether to keep your existing React UI inside a native shell. That is a reasonable engineering choice. It is not a PWA reading Apple Health.

Why the web has no surface

HealthKit’s job is to keep health samples on the device, behind a permission sheet Apple draws, in a store other apps can write to and the user can inspect in the Health app [1]. Apple’s privacy model for that store includes an asymmetry we keep repeating because it is easy to forget: the system will not tell your app whether a read was denied, because telling you would leak the denial [4]. Extending that store to fetch() from an origin you control would punch through the model.

There is also no secret REST door. Apple syncs Health data across a person’s own devices. That is not an API for you. Guideline 5.1.3(ii) separately says apps “may not store personal health information in iCloud” [5], which is a review rule about your use of iCloud, not a description of Apple’s own sync. Neither fact produces a Bearer token.

Clinical Health Records (FHIR, after the user connects a health system in the Health app) are still HealthKit types, still on-device, still behind a tighter usage string (NSHealthClinicalHealthRecordsUsageDescription). They are not a web EHR pull.


Capacitor, Ionic, and the “it’s just JavaScript” mistake

Ionic’s @capacitor/health-fitness and Capawesome’s @capawesome-team/capacitor-health are explicit: not available on web [2] [3]. The iOS path requires:

  • the HealthKit capability (com.apple.developer.healthkit)
  • background delivery if you want observer-query wakes (com.apple.developer.healthkit.background-delivery) [6]
  • NSHealthShareUsageDescription / NSHealthUpdateUsageDescription
  • on Android, Health Connect permissions in the manifest, because those cannot be requested the way Capacitor plugin config is read [2]

Capawesome’s own FAQ: “There is no web API for health data, so all methods reject with an unimplemented error on the web” [3]. Perfood’s older HealthKit plugin notes the same empty-array behaviour as native HealthKit when the user has denied read: you will not know [7]. The plugin also does not add the HealthKit capability for you. After npx cap sync, open the iOS project in Xcode and enable HealthKit (and Background Delivery, if you need it) on the target [6]. Testing in the browser, or in Expo Go, is testing a host that never received that entitlement.

Once that binary exists, App Review applies in full. Guideline 2.5.1: HealthKit must be used for health and fitness purposes and integrate with the Health app. 5.1.2(vi) and 5.1.3(i): no marketing, advertising, or use-based data mining on that data, including by third parties [5]. A Capacitor app does not get a lighter 5.1.3 because the screens are React. See the rejection field guide.


Android is the same shape

Health Connect is on the phone. Permissions are runtime, declared in the manifest and again in the Play Console Health apps form [8] [9]. A Chrome tab cannot call it.

The cloud-shaped thing on Google’s side is the Google Health API: OAuth 2.0, Restricted scopes, documented against Fitbit devices, Pixel watches, and manual entries [10]. It is a reasonable web backend integration for those sources. It is not Health Connect, and it is not a Galaxy Watch that only wrote into Health Connect on the handset.

So a “web-only, both platforms” product has no on-device health store on either side.


Two architectures that ship

A. Native reader, existing web product

Keep the coaching UI on the web. Ship a small iOS app (and, if you care about Android on-device data, an Android app) whose job is the reader, not the product. The sequence is boring and specific:

  1. HealthKit capability in Xcode (the Capacitor plugin does not flip this for you), usage strings, background-delivery entitlement if you will sync while backgrounded [6].
  2. A priming screen, then requestAuthorization. Treat empty reads as ambiguous [4].
  3. Incremental cursor, persisted: HKAnchoredObjectQuery on iOS (HealthKit has no change-token API; the anchor is the cursor). On Android, getChangesToken / getChanges — and those tokens expire on the order of a month, which is a different clock from Health Connect’s 30-day history permission [14].
  4. Persist samples locally first. POST batches to the backend you already have, with each sample’s UUID as an idempotency key. Background delivery retries; a 500 from your API must not duplicate the night.
  5. Register observer queries at launch. Call the observer completion handler on every path, including errors, before you wait on the network. Hold that handler open across a POST and HealthKit will back off; fail it three times and iOS stops waking you [15]. Upload with a background URLSession.

Capacitor is one way to keep your web components inside that reader. Swift/Kotlin is another. React Native, Flutter, and Expo prebuild are others. The architectural decision is “we have a native binary,” not which UI kit it uses. Expo Go is not that binary: it is a prebuilt host without your HealthKit entitlement. npx expo prebuild (or a bare workflow) is.

This is the only way an Apple Watch–only user ever appears in your database. Watch samples are HealthKit samples [11].

B. Cloud wearables only

Your web app runs OAuth against Oura, WHOOP, Withings, Fitbit-via-Google, Garmin-if-you-are-in. No App Store. No HealthKit sheet. No Apple Watch. No phone steps. You are building for the subset of users who own a cloud-connected wearable and whose vendor will still admit you. Garmin’s Connect Developer Program is paused for new applicants. Oura starts at ten users [12]. WHOOP requires App approval beyond ten members [13].

That is a real product for a real audience. It is not “we support Apple Health.”

Mixing A and B is normal. Native for HealthKit / Health Connect, OAuth for the clouds that have one. That is the four-model architecture, not a compromise. What does not work is drawing B, labelling it “wearables,” and discovering in month four that half the waitlist only has an Apple Watch.

The decision

If a majority of your users are on iPhone, and you want Watch or even phone-motion data, you need a native surface. The web app can remain the product. It cannot remain the only binary.

If you are unwilling to ship iOS, cut Apple Health from the spec in writing, including in sales. Then the OAuth subset is the product, with the partner-program risk that implies.

A layer such as Sahha does not create a browser HealthKit. It still has to run an SDK on the device for the on-device stores. What it changes is whether you write the query, the background delivery, the upload, and the schema. The platform constraint is Apple’s.

References

  1. Apple. HealthKit. On-device framework; no server-side API for third-party backends. https://developer.apple.com/documentation/healthkit
  2. Ionic / Capacitor. @capacitor/health-fitness: “Access Android Health Connect and Apple HealthKit data… Not available on web.” https://capacitorjs.com/docs/apis/health-fitness
  3. Capawesome. Capacitor Health plugin FAQ: no web API; methods reject as unimplemented on web. Requires Capacitor 8+. https://capawesome.io/docs/sdks/capacitor/health/
  4. Apple. Authorizing access to health data — read permission is not queryable via authorizationStatus(for:). https://developer.apple.com/documentation/healthkit/authorizing-access-to-health-data
  5. Apple. App Store Review Guidelines, 5.1.3(ii) (no personal health information in iCloud), 2.5.1, 5.1.2(vi), 5.1.3(i). Retrieved 17 September 2026. https://developer.apple.com/app-store/review/guidelines/
  6. Apple. Configuring HealthKit access, including Background Delivery entitlement. https://developer.apple.com/documentation/xcode/configuring-healthkit-access
  7. perfood/capacitor-healthkit. If access has been denied, queries return empty arrays and the app cannot distinguish denial. https://github.com/perfood/capacitor-healthkit
  8. Android Developers. Get started with Health Connect. Last updated 8 September 2026. https://developer.android.com/health-and-fitness/health-connect/get-started
  9. Android Developers. Publish your health app on Google Play — declare Health Connect data types in Play Console. https://developer.android.com/health-and-fitness/health-connect/publish
  10. Google for Developers. About the Google Health API. https://developers.google.com/health/about
  11. Sahha. Apple Health without an Apple Watch — what the phone actually writes, and what requires a Watch. https://sahha.ai/blog/apple-health-without-apple-watch/
  12. Oura. API applications start with a ten-user limit; production requires approval. Gen3 and later without Membership cannot be read through the API. https://cloud.ouraring.com/v2/docs and https://support.ouraring.com/hc/en-us/articles/4415266939155-The-Oura-API
  13. WHOOP. App Approval: 10 members in development; public launch requires approval. https://developer.whoop.com/docs/developing/app-approval/
  14. Android Developers. Synchronize data with Health Connect — change tokens, getChanges, expiry after an extended period. https://developer.android.com/health-and-fitness/health-connect/sync-data
  15. Apple. HKObserverQueryCompletionHandler. Call the block when processing is done; three unanswered wakes and background delivery stops. https://developer.apple.com/documentation/healthkit/hkobserverquerycompletionhandler

Related