github the-momentum/open-wearables 0.10.0
0.10 - New dashboard & nutrition data

5 hours ago

We encourage you to use our official Docker images:

Highlights

This release brings a rebuilt dashboard and support for nutrition data, with 42 new data types. Outgoing webhooks now tell you which sync each event came from, and calories and data sources are more accurate across providers. Plus a lot of smaller fixes and improvements, 123 PRs in total.

0.11 is coming very soon and focuses on further data model improvements, as we head towards Open Wearables 1.0. If there's something you think we should include or consider for 1.0, let us know on Discord or in Discussions. Feedback from teams that have been running Open Wearables for a while is especially welcome.

Caution

Breaking changes in 0.10.0. Most of them probably don't affect you at all, as they only touch specific providers or edge cases, but read the list carefully before upgrading.

  • active_calories_kcal is now null instead of 0 when a day has no active energy, for every provider. total_calories_kcal in /summaries/activity is null when a day has neither total nor basal energy. (#1801)
  • Whoop cycle energy is stored as the new total_energy series instead of active_energy, because it includes BMR. Webhooks for Whoop cycles now send series.total_energy.created instead of series.active_energy.created. A startup migration relabels stored rows and drops 0 kJ no-wear cycles. (#1801)
  • Ultrahuman active minutes move from active_time to exercise_time, and outgoing webhooks send series.exercise_time.created instead of series.active_time.created. active_minutes in /summaries/activity falls back to the step-based estimate for Ultrahuman users. A startup migration relabels stored rows. (#1765)
  • Ultrahuman HRV moves from heart_rate_variability_sdnn to heart_rate_variability_rmssd (confirmed by Ultrahuman). A startup migration relabels stored rows. Users whose only HRV source is Ultrahuman now get a Resilience score. (#1686)
  • Workouts from the mobile SDK and Apple XML import that report no calories now have null calories instead of 0. A startup migration clears the stored zeros. (#1874)
  • SDK refresh tokens of deleted applications are removed. Their users get a 401 on the next refresh and the SDK reports an auth error (Handle expired sessions). (#1841)
    • Check for SDK users of deleted applications
      SELECT rt.app_id,
             count(*) AS active_tokens,
             count(DISTINCT rt.user_id) AS users,
             max(coalesce(rt.last_used_at, rt.created_at)) AS last_activity
      FROM refresh_token rt
      LEFT JOIN application a ON a.app_id = rt.app_id
      WHERE rt.token_type = 'sdk'
        AND rt.revoked_at IS NULL
        AND a.id IS NULL
        AND split_part(rt.app_id, ':', 1) NOT IN ('admin', 'invite')
      GROUP BY rt.app_id
      ORDER BY last_activity DESC;

      Apps that handle the SDK auth error by getting new tokens from their backend and passing them to updateTokens() recover on their own, as long as that backend uses the credentials of an application that still exists.

  • Outgoing webhook payloads are kept in Svix for 7 days instead of 90. Change it with OUTGOING_WEBHOOK_PAYLOAD_RETENTION_DAYS (5-90). Payloads already stored keep their 90-day expiry. (#1719)
  • 24/7 sync results have a new, typed shape. data_247 is now an object with status, item counters and one entry per provider task, replacing the flat map of task names to row counts and the success/saved flags. It changes in the sync endpoint response (details.data_247), in sync.completed / sync.failed webhooks (metadata.params.data_247) and in sync status events. A sync where every 24/7 task fails now returns success: false and emits sync.failed. Error fields carry a short code (http_401, IntegrityError) instead of the exception text, and error in params.workouts is renamed to error_code. (#1501)

Note

The first startup after upgrading can take longer than usual: data migrations merge duplicated data sources and relabel the rows listed above. Expensive migrations are now recorded in a new data_migration table and run once per instance (Data migrations).


New dashboard

The dashboard is rewritten in SvelteKit by @KaliszS 🪄. The layout is cleaner, there are light and dark themes (or it follows your system setting), and it works on phones too, with a bottom navigation bar.

Heavy pages load faster, and Data Summary opens on the last 30 days by default. Tabs on the user profile have period presets (1W / 1M / 3M / 6M / 1Y) and a provider filter, and the Sleep tab can also show only night sleep, only naps, or only the highest-priority source. Each record shows which provider and device it came from.

The user profile also got a Meals tab, a Women's Health tab that's always visible (empty when there's no data), and data sources listed in priority order.

Dashboard User profile - Data Summary

User profile - Sleep User profile - Meals

User profile - Sleep

Note

The image name, port 3000 and VITE_API_URL are unchanged, so no configuration change is needed. Developers sign in again once after the upgrade. VITE_API_URL is now read at runtime, so one image works against any backend without a rebuild. Optionally set API_URL when the frontend can reach the API on an internal address, e.g. http://app:8000 on the same Docker network.

More: Frontend configuration


Nutrition data

Open Wearables now stores nutrition data, thanks to @McArthur955 🎉. There are 42 new series types: calories, carbs, protein, fat (total, saturated, mono- and polyunsaturated, trans), fiber, sugar, cholesterol, caffeine, and a full set of vitamins and minerals. Apps that log food as meals also get a new meal event type with a title, meal type and nutrients. Not every app does that, so nutrition samples logged outside a meal are stored as regular time series.

This is most useful with Apple Health. Food trackers like MyFitnessPal, Lose It! and Cronometer, and dozens of other apps, write what users eat into it, so you get their data without integrating each app separately. Apple Health covers 38 of the nutrition types, Health Connect all 42 and Samsung Health 17. The Google Health API nutrition log is supported too.

Note

For the mobile SDK, nutrition is supported on the backend side only for now. SDK versions that send meals from Apple Health, Samsung Health and Health Connect haven't been released yet, but they're coming soon - follow the iOS SDK and Android SDK repos. Google Health API nutrition works with this release.

GET    /api/v1/users/{user_id}/events/meals?start_date=2026-10-05&end_date=2026-10-05
DELETE /api/v1/users/{user_id}/events/meals/{meal_id}

The seed generator can create meals too, with a nutrition_focused preset.

More: Nutrition data types

Outgoing webhooks know which sync they came from

Every data webhook now carries a sync block, so a consumer can tell a historical backfill from live sync:

"sync": { "run_id": "backfill_9f3c8ea4", "source": "backfill", "scope": "historical" }

source is pull, webhook, sdk, backfill, xml_import or linked_account, and run_id joins to GET /sync/history/{run_key}. Two new events, sync.data_type.completed and sync.data_type.failed, report the result per data type, with item counts and the written and covered time ranges. Existing payloads only gain the new key.

Calories: total, active and basal

Providers that report only a total with no active/basal split now write to a new total_energy series (today that's Whoop), with a series.total_energy.created webhook. Basal energy is now stored for Garmin, Polar, Oura and SensorBio, and total_calories_kcal in summaries comes from total_energy first, with active + basal as the fallback. Whoop cycle metrics are also anchored on the right local day, so users in negative UTC offsets no longer get two cycles summed into one day.

Stable data sources

One device or app no longer splits into several data sources. A source is now matched by device ID, then by app bundle ID and model, and an existing source gets upgraded when better data arrives. In practice: renaming an iPhone, upgrading the phone that relays third-party Apple Health data, or Samsung sending steps without IDs no longer creates a new source. A startup migration merges the duplicates already stored. New device types were added as well: chest straps, earbuds, tablets, HR sensors, glucose meters and more.

More data from existing providers

  • exercise_time (moderate + vigorous minutes) for Garmin, Oura, Withings, Ultrahuman, Polar and Google Health
  • skin temperature deviation for Garmin, Ultrahuman and Google Health
  • per-minute activity samples for Polar (steps) and Withings (steps, distance, active energy)
  • resting heart rate for Ultrahuman and Withings
  • sleep stage timeline for Withings and SensorBio, overnight HRV from the sleep series, Polar Nightly Recharge HRV and breathing rate as time series
  • HRV RMSSD from HealthKit (iOS 27+), step count from Whoop /v2/cycle, Samsung sleep score

More: Data coverage

API additions and fixes

  • GET / PUT /api/v1/users/{user_id}/personal-record to read and set birth date, gender and sex. Until now nothing could write it, so the activity summary always assumed a 190 bpm max heart rate.
  • is_nap filter on GET /events/sleep.
  • /summaries/sleep and /summaries/recovery page by night/day instead of source row, so has_more is right when several sources recorded the same nights.
  • Out-of-range numeric dates (inf, 1e20) return 400 instead of 500, and outgoing webhook endpoints are listed past Svix's first page.

Privacy and security

  • Sentry no longer receives health data or PII by default: request bodies, stack-frame locals and log breadcrumbs are off and emails or query values are filtered. SENTRY_SEND_SENSITIVE_DATA=true turns it back on for dev/staging.
  • Deleting a user also deletes the payloads of their outgoing webhook messages from Svix.
  • Signing in to the portal with the public default password now asks you to change it. With ENVIRONMENT=production you can't skip it. More: Change Password
  • config/.env is no longer copied into locally built backend images. Images on Docker Hub never contained it.

Self-hosting

  • SMTP for invitation emails. Set SMTP_HOST to use any mail provider, Resend still works as before. When no email delivery is configured, the portal hands you the invite link instead of pretending it was sent. More: Email delivery
  • DATA_LIFECYCLE_ENABLED and OW_SCORES_ENABLED turn off Data Lifecycle (early access) and the OW sleep and resilience scores. Both default to true.
  • Logs. LOG_FORMAT=legacy|json|text and LOG_LEVEL, both opt-in, plus optional OpenTelemetry log export with OTEL_ENABLED=true, by @knowald. More: Log export
  • Containers now stop gracefully: SIGTERM is forwarded to the workers.
  • Throttled provider requests back off instead of failing the sync, and switching a provider from webhooks back to periodic pull now deregisters its webhooks.
  • Periodic sync skips push-only providers, and GET /sync/runs no longer scans every user, by @Zawiszowski.
  • All providers are seeded at startup, so priorities can be set before the first sync, and the Priorities tab shows provider logos.
  • Backend runs on Python 3.14. If you build images locally, rebuild them.
  • Extension point for add-on modules: separately packaged Python packages can register Celery tasks, periodic tasks and routers under /api/v1/ext/<name>.

Anonymous usage telemetry

Self-hosted instances now send one small anonymous usage ping a day (and one on startup). It helps us decide what to build next and what's safe to deprecate. Counts are sent as order-of-magnitude buckets and endpoints as route templates; there's no health data, user data, credentials, hostnames, IPs or URLs. Turn it off with TELEMETRY_ENABLED=false or DO_NOT_TRACK=1.

More: Telemetry

What's Changed

Breaking changes

Dashboard

  • refactor(frontend): mv React Svelte by @KaliszS in #1530
  • fix(frontend): keep sessions in an HttpOnly cookie, drop Redis by @KaliszS in #1786
  • fix(frontend): stop 500 after deploys and 404 on logout by @KaliszS in #1789
  • fix(frontend): show a notice instead of a 500 when outgoing webhooks are off by @bartmichalak & @KaliszS in #1785
  • feat(frontend): add Meals tab to the user profile by @KaliszS in #1792
  • feat: show a user's data sources in priority order by @KaliszS in #1871
  • fix(frontend): always show the Women's Health tab, empty when there i… by @KaliszS in #1880
  • fix(frontend): wait for the tab strip to mount before scrolling it in e2e tests by @KaliszS in #1881
  • chore(frontend): update dependencies by @KaliszS in #1806
  • refactor(frontend): dark theme accent color & general design changes by @KaliszS in #1883

Nutrition

Data model

  • feat: Stable data source and duplicate source merge by @czajkub & @KaliszS in #1781
  • feat: Improve device mapping inference by @czajkub in #1721
  • feat: Add extended device types (e.g. chest straps) by @czajkub in #1729
  • fix(backend): keep a data source's name and software version current by @KaliszS in #1873
  • fix(backend): keep the provider on seeded records without device info by @bartmichalak in #1671
  • feat(backend): run expensive data migrations once per instance by @KaliszS in #1864

API improvements

Outgoing webhooks

  • feat: Outgoing webhooks per-data type sync status + run tracking by @czajkub & @KaliszS in #1597
  • feat(backend): delete a user's outgoing webhook payloads from Svix on user deletion by @bartmichalak in #1809
  • fix(backend): list all outgoing webhook endpoints, not just Svix's first page by @aritsyg & @KaliszS in #1835
  • fix: upgrade svix server image to v1.101.0 by @KaliszS in #1702
  • refactor(backend): fetch event records and data sources via repositories in detail webhooks by @bartmichalak in #1842

Provider webhooks & sync

  • feat: Webhook deregistration on switch to periodic pull by @czajkub in #1675
  • fix(backend): back off on a throttled request instead of failing the sync by @GrelaM100 in #1747
  • fix(backend): log connection_revoked when a refresh token is rejected by @bartmichalak in #1714
  • fix(backend): refresh expired token before provider deregistration by @bartmichalak in #1814
  • perf(backend): remove per-user scans from periodic sync and /sync/runs by @Zawiszowski & @KaliszS in #1731
  • fix(backend): seed all providers at startup and show their logos by @bartmichalak in #1603

Multi-provider metrics

  • feat(backend): map skin temperature deviation for Garmin, Ultrahuman and Google Health by @GrelaM100 & @KaliszS in #1734
  • feat(backend): store intraday activity samples for Polar and Withings by @GrelaM100 & @KaliszS in #1746
  • feat(backend): map resting heart rate for Ultrahuman and Withings by @GrelaM100 in #1725
  • feat(backend): store the sleep stage timeline for Withings and Sensor Bio by @GrelaM100 in #1710
  • feat(backend): store overnight HRV from the sleep series by @GrelaM100 in #1756

Whoop

  • fix: Anchor Whoop cycle-related metrics on correct day by @czajkub in #1693
  • feat(backend): import step count from /v2/cycle for whoop integration by @bartmichalak in #1718

Polar

  • fix(backend): Polar - commit webhook writes and read the single-day continuous HR response by @GrelaM100 in #1699
  • fix(backend): emit Nightly Recharge HRV and breathing rate as time series by @GrelaM100 in #1722

Apple & mobile SDK

Other providers

  • fix: add HEAD probe to callback url for Withings by @czajkub in #1684
  • fix(backend): map the live_* workout sources added in Oura spec 1.40 by @bartmichalak in #1696
  • fix(backend): Suunto paginate workouts so historical sync is not capp… by @KaliszS in #1705
  • fix(backend): ignore stale Garmin deregistration pings after reconnect by @bartmichalak in #1822

Security & privacy

Platform

Other

  • refactor: add provider settings singleton, remove db/ raw repository calls from routes by @czajkub & @KaliszS in #1819
  • refactor: Add single source of truth for Celery tasks by @czajkub & @KaliszS in #1851
  • refactor(backend): remove function-level imports used to dodge circular imports by @bartmichalak in #1840
  • refactor(backend): remove the unused HR-derived HRV estimates from resilience by @GrelaM100 in #1846
  • fix(backend): make seeded users show up in recent date ranges with steps and distance by @bartmichalak in #1812
  • test(backend): block network access and ignore local .env in tests by @bartmichalak in #1823

Docs & chores

  • docs: add data flow page to architecture section by @bartmichalak in #1673
  • ci: build the API reference from a committed OpenAPI spec by @bartmichalak in #1669
  • chore(docs): regenerate OpenAPI spec after #1603 by @KaliszS in #1703
  • docs: add a shared mobile SDK integration guide and fix outdated SDK docs by @bartmichalak & @kmlpiekarz in #1700
  • feat: Document Google verification (OAuth + CASA) by @czajkub in #1698
  • docs: mention telemetry and how to opt out in the README by @bartmichalak in #1726
  • docs: note that only Google Health reads data granularity by @bartmichalak in #1727
  • docs: clarify that API credentials don't require a Whoop membership by @bartmichalak in #1778
  • docs: clarify that API credentials don't require an Oura membership by @bartmichalak in #1788
  • docs: use API_BASE_URL instead of deprecated *_REDIRECT_URI in provider guides by @bartmichalak in #1790
  • docs: update Fitbit Web API turndown date to October 30, 2026 by @bartmichalak in #1797
  • docs: compact coverage matrix with pinned headers by @bartmichalak in #1798
  • docs: add missing data types and permissions to Android and iOS … by @KaliszS in #1836
  • docs: point Fitbit users to Google Health as the successor integration by @bartmichalak in #1838
  • docs: move new provider integrations section to supported providers page by @bartmichalak in #1854
  • docs: drop unsourced Google Health onboarding pause note by @bartmichalak in #1855
  • docs: warn that seed data doesn't reflect real data volumes by @bartmichalak in #1857
  • docs: surface SDK payload offload in deployment and SDK guides by @bartmichalak in #1860
  • chore: add backward compatibility review rules to CodeRabbit by @bartmichalak in #1766
  • chore: let CodeRabbit apply PR labels automatically by @bartmichalak in #1799
  • chore: add CodeRabbit labeling instructions by @bartmichalak in #1837
  • chore: limit CodeRabbit docs requests to user-facing changes by @bartmichalak in #1843
  • chore: bump the npm-minor-patch group in /frontend with 2 updates by @dependabot[bot] in #1707
  • chore: update sentry-sdk[fastapi] requirement from >=2.69.1 to >=2.69.2 in /backend by @dependabot[bot] in #1709
  • chore: bump the python-minor-patch group across 2 directories with 5 updates by @dependabot[bot] in #1708
  • chore: update sentry-sdk[fastapi] requirement from >=2.69.2 to >=2.70.0 in /backend by @dependabot[bot] in #1755
  • chore: bump the npm-minor-patch group in /frontend with 12 updates by @dependabot[bot] in #1752
  • chore: bump pyjwt from 2.13.0 to 2.15.0 in /mcp by @dependabot[bot] in #1758
  • chore: bump virtualenv from 21.7.9 to 21.7.12 in /mcp by @dependabot[bot] in #1759
  • chore: bump the python-minor-patch group across 2 directories with 6 updates by @dependabot[bot] in #1753
  • chore: bump virtualenv from 21.7.10 to 21.7.12 in /backend by @dependabot[bot] in #1760
  • chore: bump virtualenv from 21.7.12 to 21.7.13 in /mcp by @dependabot[bot] in #1777
  • chore: bump virtualenv from 21.7.12 to 21.7.13 in /backend by @dependabot[bot] in #1783
  • chore: bump mako from 1.4.1 to 1.4.2 in /backend by @dependabot[bot] in #1787
  • chore: bump the frontend-minor-patch group in /frontend with 4 updates by @dependabot[bot] in #1868
  • chore: update OpenWearables to 0.10.0 by @KaliszS in #1884

New Contributors

Full Changelog: 0.9.0...0.10.0

Don't miss a new open-wearables release

NewReleases is sending notifications on new releases.