Last Updated: 2026-07-20
Version: 0.4
Slack: #core-health-kit-integration
Notion: Health Kit Integration
Designs
- Figma: Link
- Lifestyle Charts: GitHub
- Permissions: GitHub
Linear: HealthKit Integration
Background
As we explore a multi-modal / coaching experience, we want to collect signals that give insight into a user's overall cognitive health. The goal of this initial HealthKit integration is to get users to opt in, so we can start collecting that data and build features that surface more of that insight.
Scope (v1)
- Apple HealthKit only. Google Health Connect is deferred (decided 2026-07-09); build iOS-first.
- Metrics render on the mobile Lifestyle / Stats surface only — no web display this release. Users who connect on mobile and use us cross-platform won't see this data on web for now.
Screens
- Hook: pitches the value of connecting to Apple Health (Figma: Hook Test A–D).
- connect_disclosure: shown before the apple_native_sheet, to get final confirmation the user wants to connect.
- apple_native_sheet: Apple's own permission screen where the user grants access to the requested data. We don't control its copy or design.
- complete_connected: a variant of the check-in completed screen, shown on consent, indicating Apple Health is connected.
- settings_main_app_connected: settings component showing whether Apple Health is connected.
User Flows
Proactive touchpoint — after the last question in the self check-in flow:
flowchart LR A[Self check-in questions] --> B[Hook] B --> C[connect_disclosure] C --> D[apple_native_sheet] D --> E[complete_connected]
Evergreen — Apple connect button on the Lifestyle section of the My Brain tab:
flowchart LR A[Apple connect button<br/>Lifestyle section, My Brain] --> B[connect_disclosure] B --> C[apple_native_sheet]
Settings — from the connection status component:
flowchart LR A[settings_main_app_connected] --> B[connect_disclosure] B --> C[apple_native_sheet]
Requirements
- REQ-001 — There are 3 ways we will ask a user to connect to Apple Health: Proactive, Evergreen, and Settings.
- REQ-002 — On the hook screen, if a user taps 'Maybe Later' we do not surface the hook again during the self check-in flow. Build this flexibly so we can add logic to surface it in the future (e.g. re-ask every X check-ins).
- REQ-003 — At check-in, pre-populate the sleep question from Apple Health when data is available (labeled as sourced from Apple Health); otherwise ask the standard sleep question. Sleep reads via the same backend-first path as other metrics (REQ-011) — at check-in this is typically a direct client read, since today's data isn't in the backend yet. Whether the pre-populated value is editable is an open question (see Open questions & Follow Ups).
- REQ-004 — On connect, backfill up to 1 year of raw historical data. Backfill runs fully async (mechanics in REQ-011) so it doesn't degrade the user experience. Offline sync is deferred for v1.
- REQ-005 — Store the raw data directly from Apple as the source of truth, then transform it separately into normalized daily summaries that power the features. Stats reads the summaries, never the raw records; the transform is recomputable and versioned (non-destructive), so we can change aggregation/priority logic as we learn.
- REQ-006 — Add a new 'Lifestyle' section to the My Brain tab, with charts to display the data users self-report through check-in and Apple Health.
- REQ-007 — Ask permission for the following data: Sleep, Steps, Exercise minutes, Mindful minutes, Heart rate variability, Resting heart rate, Active Energy Burned (
activeEnergyBurned), Basal Energy Burned (basalEnergyBurned). The two energy metrics are ingested as raw data only, not charted in v1 — a metabolic signal seeding future brain-score work. - REQ-008 — On the 'Lifestyle' section, display a chart for each metric over a rolling 7-day window, per the table below. Charts are not clickable/tappable in v1 and have no x-axis — the visual conveys a trend/snapshot only, not something to analyze deeply.
Bars show the last 7 days; each card's summary statistic window (all data vs. rolling 7/30 days) is an open question (see Open questions).
| Metric | Source | Chart (bars) | Summary statistic |
|---|---|---|---|
| Sleep | Apple Health or check-in | Time slept per day; each day = the prior night | Average, ignoring days with no sleep |
| Steps | Apple Health | Steps per day | Daily average, ignoring days with no steps |
| Exercise Minutes | Apple Health | Minutes per day | Daily average |
| Mindful Minutes | Apple Health | Minutes meditated per day | Daily average |
| Resting Heart Rate | Apple Health | Latest-value snapshot — no chart to start | — |
| HRV | Apple Health | Latest-value snapshot — no chart to start | — |
| Mood | Check-in | Daily series; higher = more positive | Most-common mood; ties → better mood |
| Mental Clarity | Check-in | Daily series; higher = more clarity | Most-common clarity; ties → greater clarity |
- REQ-009 — When a metric has multiple sources, resolve to a single value per metric per day (mechanics in REQ-012). For sleep, the self check-in answer supersedes all wearable sources:
check-in answer > wearable > watch > phone. Selection priority is configurable (REQ-005), so we can change it as we learn. - REQ-010 — The proactive touchpoint ships as an A/B test measuring opt-in rate from
hook → complete_connected. Test hook variants A (Convenience), B (Insight), C (Enhancement). - REQ-011 — Health data uses two flows:
- (a) Backfill (async) — on connect, the client uploads up to a year of raw data to S3; a scheduled data job writes aggregates to Postgres (REQ-004).
- (b) Chart display — backend-first with on-demand client fallback — the default is to read from the backend. On a chart-data request for a date range, the backend serves Postgres if processed data exists for that range; if not (the job hasn't caught up), the client fetches that range directly from the health platform and pushes the raw payload to the backend, which computes the aggregates on the fly with the same logic, returns them to the client, and persists them. Same behavior when paging to older weeks. Because charts show rolling averages, job lag barely moves the number, and the fallback ensures a newly-connected user never sees an empty/stale screen.
- REQ-012 — Multi-source handling by metric type (store all raw sources regardless):
- Cumulative (Steps, Exercise minutes, Active/Basal energy) — use Apple's de-duplicated value from
HKStatisticsCollectionQuery, which matches the Apple Health app. Never sum raw samples across sources. - Metrics Apple doesn't resolve for us (Sleep, Mindful minutes, Resting HR, HRV) — select a single source per metric per day via our own ranking dedicated wearable > Apple Watch > phone (inferred from
HKDevice/sourceRevision); tiebreak within a tier: most coverage → most samples → most recent. For discrete metrics (Resting HR, HRV), take one representative value from the chosen source (Apple's derived daily value if present, else most-recent for the v1 snapshot) — never average across devices (methodologies differ). Sleep additionally applies the check-in priority in REQ-009.
- Cumulative (Steps, Exercise minutes, Active/Basal energy) — use Apple's de-duplicated value from
States
Self check-in flow
- Sleep question pre-populated — when sleep comes from Apple Health, show a special state on the sleep check-in screen.
Lifestyle screen
- When not connected to Apple Health, show an evergreen Apple Health connect card.
- When connected, show a connected tag.
- When the user hasn't completed today's self check-in, show a tappable card to complete it.
Charts
- Not connected — show an empty state previewing the charts the user would get by connecting.
- Connected — show a connected tag, then the data states below.
- Full / Partial data — all cards populated vs. only some metrics have data.
- Empty data — whole screen (no metrics yet) or per-card. For HealthKit-only charts (steps, exercise minutes, mindful minutes, resting heart rate, HRV), hide a chart with no data in the window rather than showing an empty card.
- Today — not yet checked in — show a tappable card to complete the self check-in.
- Fetching data — connected, but data is updating or an on-demand fetch (REQ-011) is in progress.
- Onboarding — a one-time popup / speech bubble explaining the summary statistic, so it isn't repeated on every card.
Sleep
- Source selection: per REQ-009 the check-in answer wins; otherwise use one source's sessions for the night — never stitch sessions across sources (double-counts). HealthKit gives no main/nap flag or per-day "time asleep," so the rule below is derived on our side from raw
HKCategoryValueSleepAnalysissamples. - Attribution rule: attribute sleep to the day the user woke up. Group sessions into a 6 PM → 6 PM window (the window ending at 6 PM on a day is that day's "sleep"); the single longest asleep session is that day's main sleep (the headline number). Naps (non-main sessions of ~20 min or more) are tracked separately and excluded from the headline.
- Nap ceiling (~3 h): a daytime session longer than ~3 h is a candidate main sleep, not a nap (Oura/Garmin).
- Segmented-sleep stitch: merge sessions separated by < ~50–60 min awake into one main sleep (Fitbit/Whoop), so a mid-night wake doesn't split it before the longest-session pick.
- Worked example — sleeps Sun 10pm–Mon 6am, then Mon 8pm–11pm: the 8-hour block is Monday's main sleep; the 8–11pm block falls into Tuesday's window as a nap.
- This evening-anchored, longest-session-wins model follows Oura's convention; the 6 PM anchor is Oura's (Apple uses the user's Sleep Focus schedule, not a fixed time). See Sources.
Events
Follows the Product instrumentation guide. Use the standard event names (screen_view, object_view, object_click) with product-specific state in properties — do not invent bespoke event names. Reuse canonical screen_name / click_name values from consumer-mobile/lib/analytics/events.dart; where no canonical name exists, record it here as a dictionary gap rather than inventing an alias. Treat assignment, exposure, and outcome as separate signals (required for the hook A/B readout).
Proactive hook (A/B)
experiment_assignment— when the user is bucketed into the hook test.screen_viewwherescreen_name = health_hook— exposure. Props:variant(A|B|C),entry_point = proactive.object_clickwhereclick_name = health_hook_connect|health_hook_maybe_later. Props:variant.
Disclosure → native sheet → complete (shared across all entry points)
screen_viewwherescreen_name = health_connect_disclosure. Props:entry_point(proactive|evergreen|settings).object_clickwhereclick_name = health_connect_disclosure_connect— user taps Connect (which opens Apple's native sheet).object_clickwhereclick_name = health_connect_disclosure_later— user taps Later- Native-sheet outcome — Discuss what is possible
screen_viewwherescreen_name = health_complete_connected.
Evergreen & Settings surfaces
object_viewwhereobject_name = health_connect_card. Props:screen_name(my_brain|settings),entry_point.object_viewwhereobject_name = health_connect_status— Settings connected state.object_clickwhereclick_name = health_manage_in_apple_health. Props:destination = apple_health.
Lifestyle charts & check-in
object_viewwhereobject_name = lifestyle_health_chart— one per rendered metric. Props:screen_name = stats,metric.- Sleep pre-fill at check-in — add a bounded property to the existing check-in event (e.g.
sleep_source = health_kit) rather than a new event.
Sync (server-owned)
- Reuse / extend a backend sync event with bounded properties (metric counts, backfill days, and on-demand fallback hits per REQ-011) instead of a new client event.
Open questions & Follow Ups
- Sleep editing at check-in — should the Apple-Health-prefilled sleep value be editable? Recommendation: editable. Check-in already shows an editable sleep question when no Apple Health data exists, so a locked prefill is the inconsistent path; the check-in answer is that day's source of truth (REQ-009), and locking risks a bad first impression when Apple Health misattributes sleep. Amit to confirm with the team.
- Summary statistic window — should the card summary (e.g. "Most nights, you sleep…") cover all available data or the rolling 7/30 days? Recommendation: window-agnostic copy backed by an all-data average, with an onboarding explainer stating the window (avoids sparse-week instability). Use a "This month…" framing if a stated window is preferred. Amit/Jennie to confirm.
- Offline support (Laxmi) — likely not needed for v1. Direction: if chart data is cached, render whatever we have; if not, show an offline state. Discuss.
- Laxmi to investigate
activeEnergyBurnedvsbasalEnergyBurned— what each reflects and whether we need both. - Jennie to design the Fetching-data and Onboarding chart states.
- Amit to follow up with Privo to figure out whether we need special privacy copy on connect_disclosure.
- Amit to discuss with Laxmi what tracking is possible for the native sheet
- Validate the 6 PM → 6 PM window, the ~20-min nap floor / ~3 h nap ceiling, and the segmented-sleep stitch against actual HealthKit sample behavior. Fixed 6 PM is a known edge-case risk (shift workers, boundary nappers) accepted for v1.
Sources
- Slack
#core-health-kit-integration(C0BAN936M6G): kickoff/overview thread (2026-07-07), lumosagent backend-architecture thread. Note: v1 is Apple only — Google Health Connect deferred (2026-07-09); wearable/isPaireddetection is Apple Watch–only. - Meeting: Prod Dev Working Session + Prod Design, 2026-07-02 (
pm/meetings/…2026_07_02…Gemini.md) — Jennie design review + team alignment; Health Kit Kickoff, 2026-07-09. - Notion: Health Kit integration Pulse row; row-owned Wearables V1 Apple Health RWE Integration v0.1. Instrumentation per Product instrumentation guide.
- Figma: Apple Health Integration —
Stats - Evergreen Touchpoint; Apple Health Flows FigJam. - Slack
#core-health-kit-integration(v0.4 inputs): sleep data flow & check-in editing; calories / energy-metric selection (addactiveEnergyBurned+basalEnergyBurned, keep steps); multi-source resolution & dropping thewearable > watch > phonescheme; chart states & summary-statistic window. - Meeting: Daily Catchup 2026-07-16 (
~/Downloads/Daily Catchup - 2026_07_16 … Gemini.md) — Health Kit metrics & data aggregation; async backend processing; Apple de-dupes steps/exercise. Plus the health-data sync MoM (two-flow architecture: async backfill + backend-first chart reads with on-demand client fallback). - Research (multi-source handling): Apple WWDC 2014 Session 203; Apple Support 108779; Google Health Connect — Read aggregated data; Rook data-processing. Findings:
HKStatisticsCollectionQueryde-dupes cumulative metrics to match the Health app; the user's source-priority order is not exposed via the HealthKit API; the 6 PM→6 PM / longest-session sleep rule matches Oura.