---
title: "Architecture"
canonical: "https://aifitnessapi.com/architecture"
type: "cluster-index"
pages: "15"
last_reviewed: "2026-08-12"
publisher: "AIFitnessAPI"
---

# Architecture

> 15 pages. Each entry below shows the question the page owns, followed by its answer capsule.

## Incremental Sync: Reading Only What Changed Since Last Time

- Question: how to sync only new health data since last sync
- HTML: https://aifitnessapi.com/architecture/incremental-sync
- Markdown: https://aifitnessapi.com/architecture/incremental-sync.md
- Last reviewed: 2026-07-27

Use the platform's own change cursor to detect what moved, and treat the result as a list of days that are now wrong rather than a list of values to add up. On iOS that is an HKQueryAnchor, which is an opaque position in the store's change log and not a timestamp; on Android it is a Health Connect changes token, which Google documents as expiring within 30 days of going unused. The constraint driving the whole design is that health data is retro-edited: sleep gets revised after further processing, watches backfill late, users correct old workouts, and Apple's own condenser rewrites months-old records. So mark the affected days dirty and recompute them from raw data, rather than incrementing a running total that no timestamp watermark can ever repair.

## Backfilling Years of Wearable Data Without Hitting Rate Limits

- Question: backfill years of wearable data rate limit
- HTML: https://aifitnessapi.com/architecture/historical-backfill
- Markdown: https://aifitnessapi.com/architecture/historical-backfill.md
- Last reviewed: 2026-07-27

Design a multi-year first sync as a resumable job, not a loop: order it newest-first so the app is useful within seconds of connecting, chunk it by civil-date window, and commit a checkpoint after every chunk so a crash costs you one window instead of the whole history. The binding constraint is that the quota is per consented user, so you cannot buy your way out of it with more workers, and on Health Connect Google publishes no numbers at all. Spend a deliberate share of that budget and reserve the rest for live sync, rather than letting the backfill starve today's data to complete 2019.

## Background Sync That Does Not Depend on the Phone Waking Up

- Question: healthkit background delivery not firing
- HTML: https://aifitnessapi.com/architecture/background-sync
- Markdown: https://aifitnessapi.com/architecture/background-sync.md
- Last reviewed: 2026-07-27

Treat every background wake as an opportunistic hint, never as a delivery guarantee. Apple documents only an upper bound on HealthKit background delivery (at most once per period, hourly-capped for step count on iOS) and a shutdown after three unacknowledged deliveries, while Health Connect has no new-data callback at all. That single constraint drives the design: pair each wake with a foreground reconciliation on the next app open and a server-side staleness check that marks a user's data unknown rather than zero. Do that, rather than running a nightly job that assumes last night's wake fired.

## Webhook ingestion for health data: making at-least-once delivery safe

- Question: health api webhook idempotency duplicate events
- HTML: https://aifitnessapi.com/architecture/webhook-ingestion
- Markdown: https://aifitnessapi.com/architecture/webhook-ingestion.md
- Last reviewed: 2026-07-27

Give the delivery and the effect separate idempotency layers: dedupe the POST on (provider, delivery_id), and make the resulting write a versioned replace on (user_id, provider, metric, local_date, source_id). The constraint driving this is that most fitness webhooks are thin change pointers rather than data, so the handler's real job is to enqueue a fetch — and a handler slow enough to do that fetch inline is what triggers the provider retries that manufacture your duplicates. Replace the day, never increment it, and order on the provider's version rather than on arrival time.

## Mapping Users to Wearable Provider Accounts and Devices

- Question: map users to wearable provider accounts multiple devices
- HTML: https://aifitnessapi.com/architecture/identity-and-account-linking
- Markdown: https://aifitnessapi.com/architecture/identity-and-account-linking.md
- Last reviewed: 2026-07-27

Model three entities, not one: the person in your product, the grant you hold from a provider, and the source that produced each sample. The constraint that forces the split is that health data is attributed at the source level, not the account level, so the mapping is many-to-many in both directions. Put a connection table between users and provider accounts rather than hanging an access token and a provider user id off your users row. The version without it cannot represent one human with two Fitbit accounts, a household sharing a scale, or a merge, and the merge is where it silently doubles someone's step and calorie history.

## Deduplicating Health Data From Multiple Sources

- Question: healthkit duplicate steps multiple sources
- HTML: https://aifitnessapi.com/architecture/deduplicate-health-data
- Markdown: https://aifitnessapi.com/architecture/deduplicate-health-data.md
- Last reviewed: 2026-07-27

Do not assume the platform deduplicates for you. HealthKit merges overlapping sources only inside statistics-query results and only for quantity types, and Health Connect dedupes only Activity and Sleep, only through the Aggregate API, using a priority order that only the end user can change. So ask the platform for the merged figure where it can give you one, and build your own resolution everywhere else: workouts, cross-provider totals, and every non-Activity type on Android. The algorithm that works is interval-wise rather than device-wise. Rank sources per user per metric, cut the day at every sample boundary, let the highest-priority source covering each sub-interval win, and never sum per-source totals.

## Normalizing wearable data across providers

- Question: normalize wearable data across providers
- HTML: https://aifitnessapi.com/architecture/normalize-wearable-data
- Markdown: https://aifitnessapi.com/architecture/normalize-wearable-data.md
- Last reviewed: 2026-07-27

Store the measurement definition beside every value, not just the metric name. Units and field names are mechanical; the layer that breaks you is that Apple HealthKit stores HRV as SDNN while Android Health Connect stores RMSSD, and those are different measurements with no conversion between them. So a canonical record carries provenance — source app, device, measurement definition, recording method and read path — as first-class columns. Do that, not a single normalized hrv column that silently mixes incompatible measures.

## Timezones and Day Boundaries: Whose Midnight Defines the Day?

- Question: fitness app daily totals timezone local midnight
- HTML: https://aifitnessapi.com/architecture/timezones-and-day-boundaries
- Markdown: https://aifitnessapi.com/architecture/timezones-and-day-boundaries.md
- Last reviewed: 2026-07-27

Store three things on every sample: the UTC instant, the UTC offset in effect at that instant, and the civil local date you compute from the two at ingest. The constraint driving it is that a daily total is a calendar question, not a time-range question - UTC alone throws away information you cannot recover, and local time alone is ambiguous on the autumn DST transition and impossible on the spring one. Then decide deliberately whose midnight defines the day; our default is the zone in effect at each sample's own timestamp. Write the local date as a real indexed column and group on it, rather than converting on read.

## Missing Data and Gaps in Health Metrics

- Question: missing days step data fill gaps health app
- HTML: https://aifitnessapi.com/architecture/missing-data-and-gaps
- Markdown: https://aifitnessapi.com/architecture/missing-data-and-gaps.md
- Last reviewed: 2026-07-27

Store absence as absence: model every user-metric-day cell as measured-with-a-value or unknown-with-a-reason, and never write a zero you did not observe. The constraint driving it is that "the user did nothing" and "we have no data" are different facts about a person, and both mobile platforms hand you the second one disguised as the first. Apple documents that a denied read permission is indistinguishable from an empty store, and Google documents a default 30-day read-history window beyond which older data is absent rather than zero. Do carry a status column and a coverage count into your daily rollup; do not zero-fill, and do not interpolate.

## How Should You Store Health Time-Series Data?

- Question: database schema for storing heart rate time series app
- HTML: https://aifitnessapi.com/architecture/time-series-storage
- Markdown: https://aifitnessapi.com/architecture/time-series-storage.md
- Last reviewed: 2026-07-27

Store raw samples immutably in one table and make every daily figure a recomputable function of them, rather than a counter you increment at ingest. The constraint driving that is that health samples are not append-only: they arrive late, users edit them months later, and Apple documents that HealthKit itself re-condenses workouts at least a few months old and deletes the originals. So a rollup over yesterday can become wrong after yesterday has passed, and only a recompute can fix it. Do keep raw plus a rollup keyed on the civil date with a dirty-day queue; do not maintain an incremental counter you can never prove correct.

## Resolving Sync Conflicts in an Offline-First Workout Log

- Question: offline first workout logging sync conflict
- HTML: https://aifitnessapi.com/architecture/offline-first-conflict-resolution
- Markdown: https://aifitnessapi.com/architecture/offline-first-conflict-resolution.md
- Last reviewed: 2026-07-27

Carry two conflict strategies, not one, and branch between them on the provenance flag both mobile health platforms already give you. For device-sourced samples the device is authoritative, so the question is dedupe — a stable external id plus a monotonic version — not conflict. For records a person typed, last-write-wins is usually wrong: it silently deletes a set or a meal the user deliberately entered, and no fitness app has a merge-conflict UI to tell them. Model user-entered workout data as an append-only event log with client-generated IDs, so two offline devices produce a union of events rather than a fight over one row.

## Versioning Derived Metrics and Recomputing Health History

- Question: recalculate derived metrics after algorithm change backfill
- HTML: https://aifitnessapi.com/architecture/metric-versioning-and-recompute
- Markdown: https://aifitnessapi.com/architecture/metric-versioning-and-recompute.md
- Last reviewed: 2026-07-27

A derived health metric is not a number, it is a function you ran over raw samples with a specific formula version, day boundary and source-resolution policy, and every one of those inputs keeps moving after the fact. Store only the output and you can neither explain the number nor reproduce it. So stamp the formula version onto every derived row, keep the raw samples that fed it, and run recompute as a checkpointed background job with the same budgeting as backfill. Recompute deliberately and announce it; silently rewriting last year's calorie or readiness numbers is the version users actually notice.

## Monitoring a Health Data Pipeline for Silent Failures

- Question: monitor health data pipeline anomaly detection
- HTML: https://aifitnessapi.com/architecture/data-quality-monitoring
- Markdown: https://aifitnessapi.com/architecture/data-quality-monitoring.md
- Last reviewed: 2026-07-27

Treat ingestion as a monitored system with its own SLOs, and define every check per provider rather than globally. The constraint driving that is cadence: Apple delivers when the app opens or a background wake fires, Health Connect never pushes at all, a ring syncs when it is charged, so one global freshness alarm is either always firing or never firing. Measure the fraction of a provider's active users whose newest sample is older than N hours, not the time of the last row inserted, because one active user keeps a global metric green while the rest of the cohort has gone dark. Alert on the absence and the shape of data, not on exceptions, because a health pipeline's dominant failure mode is silence.

## Deleting and Exporting a User's Health Data

- Question: implement delete user health data across backend
- HTML: https://aifitnessapi.com/architecture/data-deletion-and-export
- Markdown: https://aifitnessapi.com/architecture/data-deletion-and-export.md
- Last reviewed: 2026-07-27

Model deletion as a tombstone plus a checkpointed, per-store purge job driven by a checked-in registry of stores, not as a cascade of DELETE statements and not as a script someone runs. The constraint is that a health record never lives in one place: it lives in raw samples, every rollup and continuous aggregate derived from them, caches, queues, dead-letter queues, logs that captured a provider payload, analytics, search indexes, embeddings, backups you cannot surgically edit, and an upstream OAuth grant that will refill all of it tomorrow. Revoke the provider grant first and purge second, because the reverse ordering leaves a window in which a webhook re-creates the user you just deleted. Export is the same traversal in reverse, and an export that omits derived rollups omits the only numbers the user ever actually saw.

## Caching Fitness API Responses Without Serving Stale Health Data

- Question: cache fitness api responses health data
- HTML: https://aifitnessapi.com/architecture/caching-fitness-api-responses
- Markdown: https://aifitnessapi.com/architecture/caching-fitness-api-responses.md
- Last reviewed: 2026-08-12

Cache what is not about a person as freely as you like: provider metadata, exercise catalogue rows and media are effectively immutable and belong in a long-lived shared tier. A user's settled days are cacheable only under an eviction driven by the event that says they moved, and the current day's totals — plus streaks, goals and anything that fires a notification — should be recomputed rather than served warm. Two properties make this different from ordinary API caching: health data is retro-edited, so a day you considered final can change tonight, and the day a value belongs to is a civil-calendar question, so a cached today is wrong the moment the user crosses their own midnight. Key every entry per user and per civil date, use expiry only as a backstop behind event-driven invalidation, and put your caches on the erasure inventory, because a cache is storage.
