We encourage you to use our official Docker images:
- backend: https://hub.docker.com/r/themomentum/open-wearables-backend
- frontend: https://hub.docker.com/r/themomentum/open-wearables-frontend
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_kcalis nownullinstead of0when a day has no active energy, for every provider.total_calories_kcalin/summaries/activityisnullwhen a day has neither total nor basal energy. (#1801)- Whoop cycle energy is stored as the new
total_energyseries instead ofactive_energy, because it includes BMR. Webhooks for Whoop cycles now sendseries.total_energy.createdinstead ofseries.active_energy.created. A startup migration relabels stored rows and drops 0 kJ no-wear cycles. (#1801) - Ultrahuman active minutes move from
active_timetoexercise_time, and outgoing webhooks sendseries.exercise_time.createdinstead ofseries.active_time.created.active_minutesin/summaries/activityfalls back to the step-based estimate for Ultrahuman users. A startup migration relabels stored rows. (#1765) - Ultrahuman HRV moves from
heart_rate_variability_sdnntoheart_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
nullcalories instead of0. A startup migration clears the stored zeros. (#1874) - SDK refresh tokens of deleted applications are removed. Their users get a
401on 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_247is now an object withstatus, item counters and one entry per provider task, replacing the flat map of task names to row counts and thesuccess/savedflags. It changes in the sync endpoint response (details.data_247), insync.completed/sync.failedwebhooks (metadata.params.data_247) and in sync status events. A sync where every 24/7 task fails now returnssuccess: falseand emitssync.failed. Error fields carry a short code (http_401,IntegrityError) instead of the exception text, anderrorinparams.workoutsis renamed toerror_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.
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:
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-recordto 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_napfilter onGET /events/sleep./summaries/sleepand/summaries/recoverypage by night/day instead of source row, sohas_moreis right when several sources recorded the same nights.- Out-of-range numeric dates (
inf,1e20) return400instead of500, 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=trueturns 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=productionyou can't skip it. More: Change Password config/.envis no longer copied into locally built backend images. Images on Docker Hub never contained it.
Self-hosting
- SMTP for invitation emails. Set
SMTP_HOSTto 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_ENABLEDandOW_SCORES_ENABLEDturn off Data Lifecycle (early access) and the OW sleep and resilience scores. Both default totrue.- Logs.
LOG_FORMAT=legacy|json|textandLOG_LEVEL, both opt-in, plus optional OpenTelemetry log export withOTEL_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/runsno 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
- fix: Total/basal calories ingestion by @czajkub & @KaliszS in #1801
- feat(backend): store moderate and vigorous minutes as exercise time for six providers by @GrelaM100 & @KaliszS in #1765
- fix(backend): store Ultrahuman HRV as RMSSD instead of SDNN by @GrelaM100 in #1686
- fix(backend): leave workout calories unset when SDK or Apple XML impo… by @KaliszS in #1874
- fix(backend): link SDK refresh tokens to the application that issued them by @bartmichalak in #1841
- feat(backend): set payload retention for outgoing webhooks by @bartmichalak in #1719
- refactor: Normalize 24/7 data sync results by @czajkub & @KaliszS in #1501
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
- feat(backend): support nutrition data by @McArthur955 & @KaliszS in #1674
- feat(backend): support Google Health API nutrition data by @McArthur955 & @KaliszS in #1689
- feat(backend): implement nutrition payload by @McArthur955 & @KaliszS in #1691
- feat(backend): Implement nutrition seed by @McArthur955 & @KaliszS in #1692
- fix: generate meals from presets and the seed form by @bartmichalak in #1793
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
- feat(backend): nap filter for events/sleep endpoint by @KaliszS in #1701
- feat(backend): add endpoints to read and set a user's personal record by @GrelaM100 in #1807
- fix(backend): return 400 for out-of-range numeric date params by @aritsyg & @bartmichalak in #1834
- fix(backend): page sleep summaries by night, not by source row by @aritsyg & @KaliszS in #1845
- fix(backend): page recovery summaries by day, not by score row by @aritsyg & @KaliszS in #1844
- fix: Include workout-only days in daily summary by @czajkub & @KaliszS in #1863
- fix(backend): fall back to the same writer's heart rate for workout avg HR by @KaliszS in #1866
- fix(backend): run list_users handler in threadpool again by @bartmichalak in #1848
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
- refactor: Update Apple services structure by @czajkub in #1694
- feat(backend): ingest HRV RMSSD from HealthKit (iOS 27+) by @bartmichalak in #1750
- fix(backend): declare Samsung coverage from what the SDK actually emits by @bartmichalak in #1736
- fix(backend): persist Samsung sleep score sent by the SDK by @bartmichalak in #1751
- feat(backend): log SDK attribution headers in access and SDK logs by @bartmichalak in #1811
- feat(backend): log unsupported SDK record types in import summary by @bartmichalak in #1872
Other providers
- fix: add
HEADprobe 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
- fix(backend): keep health data and PII out of Sentry events by @bartmichalak in #1791
- feat: ask for a new password after signing in with the default one by @bartmichalak in #1821
- chore(backend): keep config/.env out of the Docker image by @bartmichalak in #1849
Platform
- feat(backend): support SMTP for invitation emails by @bartmichalak in #1762
- fix: don't show invitations as sent when email delivery is not configured by @bartmichalak in #1763
- feat: env flags to turn off data lifecycle and OW health scores by @bartmichalak in #1706
- feat(backend): add opt-in LOG_FORMAT and LOG_LEVEL settings by @knowald in #1772
- chore(backend): upgrade FastAPI to 0.142.2 with its OpenTelemetry support off by @knowald & @bartmichalak in #1773
- feat(backend): add optional OpenTelemetry log export by @knowald & @bartmichalak in #1774
- fix(backend): forward SIGTERM to workers so containers stop gracefully by @knowald in #1771
- feat: anonymous usage telemetry with opt-out by @bartmichalak in #1695
- feat(backend): report SDK payload S3 offload in telemetry by @bartmichalak in #1859
- feat(backend): exclude seed users from telemetry counts by @bartmichalak in #1886
- feat: extension point for add-on modules by @bartmichalak, @GrelaM100 & @KaliszS in #1795
- chore: update Python to 3.14 by @KaliszS in #1676
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
- @GrelaM100 made their first contribution in #1686
- @Zawiszowski made their first contribution in #1731
Full Changelog: 0.9.0...0.10.0




