github the-momentum/open-wearables 0.11.0
0.11 - Daily aggregates & faster summaries

3 hours ago

We encourage you to use our official Docker images:

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=1day or on their sleep or workout. Series webhooks no longer send them with is_daily_total: true. The backfill moves the history's old day-value samples to series_sample_backup, nothing is deleted. (#1938, #1941)
  • data_point_series is renamed to series_sample, and value is now an unbounded NUMERIC. 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_days is removed, rollup_enabled and history_rollup_enabled are added, and storage.archive_* is renamed to aggregate_*. (#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, and average_met is merged into physical_effort (unit met). Old names still work in requests. See the webhook migration below. (#1937)
  • Suunto workout speed is now in m/s, and Strava kilojoules are taken 1:1 as kcal. (#1931)

Other

  • Unmeasured sleep stages are null instead of 0. A startup migration fixes stored nights and can't be undone. (#1953)
  • /health-scores returns 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. Set ADMIN_PASSWORD and restart. (#2004)
  • Connections that can no longer sync are revoked. Expect a batch of connection.revoked webhooks 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.

image

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.

Rollup and lifecycle details
  • Switching the rollup off needs 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.
  • History rollup progress is in GET /api/v1/settings/archival under history_rollup: state (off, running, done), sources_done, sources_total.
  • Backfill progress shows in the logs as Aggregate backfill: <step> done. Until it reaches them, legacy workouts show no statistics apart from calories and average heart rate.
  • Data Lifecycle (early access) no longer has "archive after N days". Deletion removes samples only, and their days and sessions stay as aggregates. Retention waits until the backfill is done. More: Data Lifecycle
  • PUT /api/v1/settings/archival keeps the saved value of a field you leave out.
Webhook migration for the renamed series (optional, manual)

An endpoint filtering on 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;

Connections whose refresh token the provider rejects, or that get nothing but 401/403 for a day, are revoked too. Such syncs fail with reauth_required.


Other changes

Sleep

  • interruptions_count in /summaries/sleep is 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_SECONDS default is now 300).
  • 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

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

Provider webhooks & sync

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

Platform

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

Don't miss a new open-wearables release

NewReleases is sending notifications on new releases.