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 is about the data model. Day and session values (a day's steps, a night's HRV, a workout's average heart rate) get their own tables, and summaries and day/week/month time series read them instead of recomputing every day from raw samples. Once everything is rolled up, the heaviest activity summary drops from seconds to tens of milliseconds. Plus fixes to sleep data, dead connections and health scores, 66 PRs in total.
This is the last big data model change before Open Wearables 1.0. Feedback is welcome on Discord or in Discussions, especially from large instances that try the history rollup.
Caution
Breaking changes in 0.11.0. Back up the database before upgrading: some startup migrations can't be undone.
Data model
- Day and session values are no longer samples. Read them with
/timeseries?resolution=1dayor on their sleep or workout. Series webhooks no longer send them withis_daily_total: true. The backfill moves the history's old day-value samples toseries_sample_backup, nothing is deleted. (#1938, #1941) data_point_seriesis renamed toseries_sample, andvalueis now an unboundedNUMERIC. Update raw SQL and BI. During a rolling deploy, pods still on 0.10 fail until they're replaced. (#1930)/api/v1/settings/archival:archive_after_daysis removed,rollup_enabledandhistory_rollup_enabledare added, andstorage.archive_*is renamed toaggregate_*. (#1932, #1933, #2026)- Some values change. Sleep physiology comes from the sleep's own device, and workouts fill statistics the provider didn't send from their own samples. (#1935, #1936)
- Renames:
walking_heart_rate_average→walking_heart_rate, andaverage_metis merged intophysical_effort(unitmet). Old names still work in requests. See the webhook migration below. (#1937) - Suunto workout speed is now in m/s, and Strava
kilojoulesare taken 1:1 as kcal. (#1931)
Other
- Unmeasured sleep stages are
nullinstead of0. A startup migration fixes stored nights and can't be undone. (#1953) /health-scoresreturns one OW score per category and day. OW native scores stop being computed on November 6, 2026. More (#1877, #1968)- In production, signing in with the default admin password returns
403. SetADMIN_PASSWORDand restart. (#2004) - Connections that can no longer sync are revoked. Expect a batch of
connection.revokedwebhooks after the upgrade. (#1894, #2021) - Stored Polar workout times and duplicated Polar nights are corrected at startup. (#1870, #2019)
- iOS SDK 0.17.0 needs this backend. Upgrade the backend first, or body fat and walking percentages get multiplied by 100 twice. (#1986)
New data model: day and session aggregates
The new data model by @KaliszS 🧱 moves day and session values out of the samples table:
| Table | Holds |
|---|---|
series_sample (was data_point_series)
| readings only, with a new window_seconds (e.g. 900 for a Garmin epoch)
|
series_aggregate
| one series over one local day: sum, avg, min, max, last, count
|
event_record_aggregate
| one series over one sleep or workout |
series_sample_backup
| the history's old day-value samples, moved out of series_sample once the backfill copied them (same columns and ids; a downgrade moves them back)
|
A row is either the provider's (computed_by: provider) or ours, rolled up from the samples (open_wearables). Readers take the provider's and fall back to ours. Days that aren't rolled up yet are counted from the samples, as in 0.10.
Measured on a database with 25.6M samples:
/summaries/activity, heavy user, 30 days 1.3–2.9 s → 25 ms
/events/workouts/totals, all time 151 ms → 74 ms
aggregates for the whole history 71 MB, +0.9 % of the database
New in the API: /timeseries?resolution=1day|1week|1month with stats and computed_by, window_seconds on every sample and series webhook, workout statistics a provider didn't send computed from the workout's own samples, and new distance and elevation_gain series for whole-workout totals. More: Workout statistics
Upgrading
| What it does | When | If you skip it | |
|---|---|---|---|
| Rollup | every 15 minutes, rolls up the days that changed | on by default, also on upgraded instances | you can switch it off in Settings → Data Lifecycle. Everything works as in 0.10, just without the speedup and without the workout statistics we compute |
| Backfill | copies the history's provider values and workout statistics into the new tables, last 30 days first | automatic, in the background, minutes | - |
| History rollup | computes our rows for days before the upgrade (Apple, Health Connect, Samsung) | manual in 0.11: Settings → Data Lifecycle, or PUT /settings/archival with history_rollup_enabled: true. Automatic in 1.0, once tested on the largest instances
| older days are read from the samples, with the same results and speed as in 0.10 |
Risks. The history rollup is heavy: about 30 minutes of database work on a copy of a production database, and hours on the largest instances. Run it when the database can take the load, and switch it off if needed, since it resumes where it left off. Until it's done, the speedup only covers new days and provider values. Downgrading is blocked once retention has deleted samples, so restore your backup instead.
An endpoint filtering on Connections whose refresh token the provider rejects, or that get nothing but 401/403 for a day, are revoked too. Such syncs fail with Rollup and lifecycle details
DATA_LIFECYCLE_ENABLED=true, because with false the settings endpoint returns 503. Days that change while it's off are not rolled up when you switch it back on.
GET /api/v1/settings/archival under history_rollup: state (off, running, done), sources_done, sources_total.
Aggregate backfill: <step> done. Until it reaches them, legacy workouts show no statistics apart from calories and average heart rate.
PUT /api/v1/settings/archival keeps the saved value of a field you leave out.
Webhook migration for the renamed series (optional, manual)
series.walking_heart_rate_average.created or series.average_met.created goes silent after the upgrade.
# 1. from the new image, before it takes traffic
uv run python scripts/data_migrations/rename_series_webhook_events.py --phase=add --dry-run
uv run python scripts/data_migrations/rename_series_webhook_events.py --phase=add
# 2. deploy 0.11
# 3. drop the old names
uv run python scripts/data_migrations/rename_series_webhook_events.py --phase=remove
series.average_met.created has no successor. The value is now average_met on the workout. An endpoint that filters only on old names is left alone, so set its new filter by hand. More: Webhooks
Check which connections will be revoked
SELECT provider, count(*) FROM user_connection
WHERE status = 'active' AND refresh_token IS NULL AND token_expires_at < now()
GROUP BY provider;
reauth_required.
Other changes
Sleep
interruptions_countin/summaries/sleepis filled in: awakenings longer than 5 minutes.- Apple Watch nights show up about 15 minutes after the upload instead of up to 3 hours later (
SLEEP_SYNC_INTERVAL_SECONDSdefault is now300). - Nights other apps write to HealthKit no longer merge into the Apple Watch night.
Providers and mobile SDK
- Polar workouts are stored at their real time, no longer twice their UTC offset in the future.
- Cycling power, cadence and speed from iOS and Samsung now reach the workout.
- Apple Watch sleeping wrist temperature is stored as
skin_temperature. - Third-party HealthKit apps get the right device type, e.g. Oura as a ring, not a phone.
- Reconnecting backfills only the gap since the last sync, and historical sync respects each provider's limit.
- Google Health API workouts and sleep are fetched in 7-day chunks within the sync window.
Self-hosting and security
- In production, startup refuses to create an admin with the default password.
- Settings → Providers warns about enabled providers without OAuth credentials.
- If you deployed from our Railway template, rotate your secrets after Railway's October security incident.
- Garmin's developer program is open again. Historical backfill needs the Enterprise tier. More
What's Changed
Breaking changes
- fix(backend): store unreported sleep stages as null instead of 0 by @McArthur955 & @KaliszS in #1953
- feat(backend): return one OW health score per day by source priority by @GrelaM100 & @KaliszS in #1877
- docs: deprecate Open Wearables native health scores by @bartmichalak in #1968
- fix(backend): refuse the default admin password at production login by @bartmichalak in #2004
- fix(backend): report dead provider connections as failed syncs by @McArthur955 & @KaliszS in #1894
- fix(backend): revoke grants rejected at the provider by @KaliszS in #2021
- fix(backend): stop Polar sleep re-syncs from duplicating a night by @GrelaM100 & @KaliszS in #1870
- fix(integrations): Polar exercises are stored 2x the UTC offset in the future by @GrelaM100 & @slavarosin in #2019
Data model
- feat(backend): aggregates: data model by @KaliszS in #1929
- refactor(backend): aggregates: rename data_point_series to series_sample by @KaliszS in #1930
- feat(backend): aggregates: providers write day and session values by @KaliszS in #1931
- feat(backend): aggregates: dirty-day rollup and retention by @KaliszS in #1932
- feat(dashboard): aggregates: lifecycle tab by @KaliszS in #1933
- feat(api): aggregates: /timeseries 1day, 1week, 1month by @KaliszS in #1934
- feat(api): aggregates: summaries, rollup every 15 min by @KaliszS in #1935
- feat(api): aggregates: workout statistics by @KaliszS in #1936
- refactor(api): aggregates: series renames with aliases by @KaliszS in #1937
- feat(backend): aggregates: backfill task, workout details, epochs, archive by @KaliszS in #1938
- feat(backend): aggregates: backfill history by @KaliszS in #1939
- feat(backend): aggregates: backfill rollup by @KaliszS in #1940
- feat(backend): aggregates: move the copied history values to series_sample_backup by @KaliszS in #1941
- feat: history rollup on demand by @KaliszS in #2026
- refactor(backend): single definition of event categories by @McArthur955 in #1895
Dashboard
- fix(frontend): backfill date range picker unreadable in dark mode by @McArthur955 in #1896
API improvements
- fix(backend): count sleep interruptions in the daily sleep summary by @GrelaM100 & @KaliszS in #1865
Provider webhooks & sync
- perf(backend): cache provider access tokens for the duration of a sync by @McArthur955 & @KaliszS in #1950
- feat: Backfill only the gap since last sync on reconnect by @czajkub & @KaliszS in #1900
- fix: Cap backfill at
max_historical_daysand change arbitrary 365 day limit by @czajkub & @KaliszS in #1862
Apple & mobile SDK
- feat(backend): gate Apple percentage scaling on iOS SDK version by @bartmichalak in #1986
- fix(backend): map SDK cycling power, cadence and speed to workout details by @bartmichalak in #1987
- fix(backend): store Apple sleeping wrist temperature as skin_temperature by @bartmichalak in #1971
- fix: Remove incorrect host model device type mapping from SDK payloads by @czajkub in #1902
- fix: One Redis state per writer app for Apple Health sleep sessions by @czajkub & @KaliszS in #1876
- fix(backend): save an Apple Watch night soon after its upload by @GrelaM100 & @KaliszS in #1892
- feat(backend): log SDK app state and sync trigger by @bartmichalak in #1980
Other providers
- feat: Google Health API workouts/sleep fetch and paging/chunking improvements by @czajkub & @KaliszS in #1688
- fix(backend): google provider split survives a retry after the seed ran by @slavarosin in #1817
Security
- fix(backend): refuse to seed a production admin with the default password by @bartmichalak & @slavarosin in #2002
Platform
- feat: warn about enabled providers without OAuth credentials by @bartmichalak in #1946
- chore(backend): remove unused async database session by @bartmichalak in #1959
- chore(backend): update FastAPI telemetry comment for 0.143 by @bartmichalak in #1962
Docs & chores
- docs: add Railway security incident warning to deployment guide by @bartmichalak in #1924
- docs: Garmin developer program reopened with Starter and Enterprise tiers by @bartmichalak in #1947
- docs: drop unshipped features from the docs homepage by @bartmichalak in #1949
- docs: align iOS SDK install constraints on 0.16 by @bartmichalak in #1988
- docs: update data types page by @bartmichalak in #2005
- docs: rework README to match the new openwearables.io by @bartmichalak in #2003
- docs: README follow-ups - deprecated Fitbit, plain title by @bartmichalak in #2007
- docs: rebrand docs to the openwearables.io design system by @bartmichalak in #1981
- docs: darken text to match openwearables.io by @bartmichalak in #2008
- chore: update OW to 0.11 by @KaliszS in #2027
- chore: update sentry-sdk[fastapi] requirement from >=2.70.0 to >=2.72.0 in /backend by @dependabot[bot] in #1923
- chore: update fastapi[opentelemetry] requirement from >=0.142.2 to >=0.143.1 in /backend by @dependabot[bot] in #1922
- chore: bump certifi from 2026.7.22 to 2026.10.19 in /mcp by @dependabot[bot] in #1956
- chore: bump opentelemetry-api from 1.44.0 to 1.45.1 in /mcp by @dependabot[bot] in #1954
- chore: bump virtualenv from 21.7.13 to 21.14.9 in /mcp by @dependabot[bot] in #1955
- chore: bump virtualenv from 21.7.13 to 21.14.9 in /backend by @dependabot[bot] in #1957
- chore: bump opentelemetry-api from 1.45.0 to 1.45.1 in /backend by @dependabot[bot] in #1960
- chore: bump certifi from 2026.7.22 to 2026.10.19 in /backend by @dependabot[bot] in #1961
- chore: bump the frontend-minor-patch group in /frontend with 5 updates by @dependabot[bot] in #1918
- chore: bump the python-minor-patch group across 2 directories with 15 updates by @dependabot[bot] in #1967
- chore: bump virtualenv from 21.14.9 to 21.14.13 in /mcp by @dependabot[bot] in #1989
- chore: bump dnspython from 2.8.0 to 2.9.0 in /mcp by @dependabot[bot] in #1990
- chore: bump authlib from 1.8.0 to 1.9.1 in /mcp by @dependabot[bot] in #1992
- chore: bump dnspython from 2.8.0 to 2.9.0 in /backend by @dependabot[bot] in #1995
- chore: bump filelock from 3.32.7 to 4.2.1 in /backend by @dependabot[bot] in #1996
Full Changelog: 0.10.0...0.11.0