All postsReleases

Open Wearables 0.11.0: daily aggregates and faster summaries

Open Wearables Team · · 11 min read

Open Wearables 0.11.0 is available. This release is about the data model. Day and session values, such as a day's steps, a night's HRV or a workout's average heart rate, get their own tables. Summaries and day, week and 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. The release also fixes sleep data, dead connections and health scores, with 66 pull requests in total.

This is the last big data model change before Open Wearables 1.0. Several changes are breaking, and some startup migrations can't be undone. Back up the database before upgrading.

Key takeaways

  • Day and session values get their own aggregate tables, and the samples table is renamed to series_sample
  • Time series can be read by day, week and month
  • Once the history is rolled up, the heaviest activity summary drops from 1.3 to 2.9 s to 25 ms
  • Workout statistics a provider didn't send are computed from the workout's own samples
  • A rollup runs every 15 minutes and is on by default. The history rollup for days before the upgrade is manual in 0.11
  • Sleep summaries count interruptions, and Apple Watch nights show up about 15 minutes after the upload
  • Connections that can no longer sync are revoked, and production refuses the default admin password

Breaking changes

Back up the database before upgrading: some startup migrations can't be undone. Most changes below touch the data model, so check raw SQL, BI and webhook filters in particular.

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, so 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 the statistics the provider didn't send from their own samples. (#1935, #1936)
  • Renames: walking_heart_rate_average is now walking_heart_rate, and average_met is merged into physical_effort (unit met). Old names still work in requests. (#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 mobile SDK, Apple XML and Suunto nights and can't be undone. Stored nights from other cloud providers are left as they are. (#1953)
  • /health-scores returns one OW score per category and day. On November 6, 2026 the OW native scores are removed from the repository, and later releases no longer compute them. (#1877, #1968)
  • In production, signing in with the default admin password returns 403. Set ADMIN_EMAIL and ADMIN_PASSWORD and restart. (#2004)
  • Connections that can no longer sync are revoked. If outgoing webhooks are enabled, expect a batch of connection.revoked webhooks as connections sync 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)

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 or 403 for a day, are revoked too. Such syncs fail with reauth_required.

New data model: day and session aggregates

Day and session values move out of the samples table into tables of their own:

  • series_sample (was data_point_series) holds readings only, with a new window_seconds, for example 900 for a Garmin epoch.
  • series_aggregate holds one series over one local day: sum, avg, min, max, last, count.
  • event_record_aggregate holds one series over one sleep or workout.
  • series_sample_backup holds the history's old day-value samples, moved out of series_sample once the backfill copied them. Same columns and ids.

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:

Open Wearables 0.11 data flow: samples, daily and session aggregates, rollup and summary endpoints
            /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
          

/timeseries by day, week and month

resolution now accepts 1day, 1week and 1month next to the existing sample buckets. These calendar periods are read from the daily aggregates, and days not rolled up yet are counted from the samples. Each point carries stats and computed_by. For daily points, computed_by tells the provider's value from ours. Weeks and months are always combined by Open Wearables.

            GET /api/v1/users/{user_id}/timeseries?types=steps&resolution=1day&start_time=2026-10-01&end_time=2026-10-31
          

window_seconds is now on every sample and every series webhook.

Workout statistics

A workout's statistics are read in this order: the provider's figure when it sent one, then ours, computed from the samples the same device recorded between the workout's start and end, then null. Ours are computed by the rollup, usually within 15 minutes of the samples arriving. A statistic the provider sent as 0 stays 0.

This covers heart rate, steps, distance and pace, speed, power, MET, cadence, elevation and calories. New distance and elevation_gain series hold whole-workout totals. /events/workouts/totals and the workout part of /summaries/activity add up the same per-workout values.

A client that read null as "the provider sent no value" now receives the computed value in the same field. More: Workout statistics

Upgrading the data model

Three processes fill the new tables:

Rollup. Every 15 minutes it rolls up the days that changed. It's on by default, also on upgraded instances. You can switch it off in Settings, Data Lifecycle. Everything then works as in 0.10, just without the speedup and without the workout statistics we compute.

Backfill. It copies the history's provider values and workout statistics into the new tables, last 30 days first. It runs automatically in the background and takes minutes. 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.

History rollup. It computes our rows for days before the upgrade, for every data source. This matters most for Apple, Health Connect and Samsung, which send no daily values of their own. In 0.11 it's manual: Settings, Data Lifecycle, or the call below. It needs rollup_enabled, which is on by default.

            PUT /api/v1/settings/archival
{ "history_rollup_enabled": true }
          

If you skip it, older days are counted from the samples as before, at 0.10 speed, apart from the value changes listed under breaking changes. Workouts from before the upgrade get our computed statistics only after the history rollup. It becomes automatic in 1.0, once tested on the largest instances. Progress is in GET /api/v1/settings/archival under history_rollup: state (off, running, done), sources_done, sources_total.

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. You can 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. Restoring the backup taken before the upgrade is the way back. The downgrade migration refuses to run once retention has deleted samples, and by default also once provider values are stored in the new tables, which happens right after the upgrade. Startup data fixes, such as the sleep stage and Polar corrections, are not reverted by a downgrade.

Data Lifecycle

  • Data Lifecycle (early access) no longer has "archive after N days". Deletion removes samples only. With the rollup on, their days and sessions stay as aggregates. Retention waits until the backfill is done.
  • 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.
  • PUT /api/v1/settings/archival keeps the saved value of a field you leave out.

More: Data Lifecycle

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 series.average_met.created is left alone, so set its new filter by hand. More: Webhooks

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. The SLEEP_SYNC_INTERVAL_SECONDS default is now 300.
  • Nights other apps write to HealthKit no longer merge into the Apple Watch night.
  • Unmeasured sleep stages are stored as null, so "no deep sleep measured" no longer reads as "0 minutes of deep sleep".

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 no longer inherit the iPhone's device type. It's taken from the app, for example Oura as a ring, or left empty.
  • Reconnecting backfills only the gap since the last sync. Historical sync is capped at the provider's limit where it has one.
  • Google Health API workouts and sleep are paged within the sync window, with a guard against a repeated page.
  • Provider access tokens are cached for the duration of a sync.

Health scores

/health-scores now returns one OW score per category and day, picked by source priority. On November 6, 2026 the OW native Sleep Score and Resilience Score are removed from the Open Wearables repository, and later releases no longer compute them. The endpoint, its parameters and response format stay the same. Scores from connected providers are unaffected, and OW scores computed before that date stay in the database and are still returned. Records with "provider": "internal" are OW native scores. More: Health scores

Self-hosting and security

  • In production, a fresh database with the default admin password stops the backend at startup, and signing in with the default password returns 403. Set ADMIN_EMAIL and ADMIN_PASSWORD to the account's email and a new password, then restart.
  • 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. More: Railway
  • Garmin's developer program is open again. Historical backfill needs the Enterprise tier. More: Garmin

Upgrading

            docker compose pull
docker compose up -d
          

Before you upgrade: back up the database, set ADMIN_EMAIL and ADMIN_PASSWORD if production still uses the default password, and run the revoked-connections query. If you filter on the renamed series, run --phase=add of the webhook rename script from the 0.11 image before it takes traffic.

After you deploy: update raw SQL and BI for series_sample, read day values with resolution=1day, check clients that read 0 for unmeasured sleep stages, and plan the history rollup for a time the database can take the load. Upgrade the backend before rolling out iOS SDK 0.17.0.

We recommend the official Docker images:

  • backend: themomentum/open-wearables-backend
  • frontend: themomentum/open-wearables-frontend

Full changelog: github.com/the-momentum/open-wearables/releases/tag/0.11.0

What's next

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.

See related articles

Frequently asked questions

Do I have to run the history rollup?

No. Without it, days before the upgrade are counted from the samples as before, at 0.10 speed, apart from the value changes listed under breaking changes. Workouts from before the upgrade don't get our computed statistics until it runs. In 0.11 it's manual. It becomes automatic in 1.0.

How long does the history rollup take?

About 30 minutes of database work on a copy of a production database, and hours on the largest instances. It resumes where it left off, so you can switch it off if the database needs the capacity.

My raw SQL reads data_point_series. What changes?

The table is renamed to series_sample, and day and session values are no longer in it. Read day values from series_aggregate and sleep or workout values from event_record_aggregate, preferring computed_by = 'provider', or use /timeseries?resolution=1day. value is now an unbounded NUMERIC.

Why did I get a batch of connection.revoked webhooks after upgrading?

0.11 revokes connections that can no longer sync: expired tokens with no refresh token, refresh tokens the provider rejects, and connections that get nothing but 401 or 403 for a day. A connection is revoked on its next sync. The query above lists the first group before you upgrade.

Can I downgrade to 0.10?

Restore the backup taken before the upgrade. The downgrade migration refuses to run once retention has deleted samples, and by default also once provider values are stored in the new tables, which happens right after the upgrade.

For questions or feedback, open an issue on GitHub or join the Discord community.

Open Wearables

Open Wearables is an open-source platform that connects your application to wearable and health data providers through a single API. One normalized data model, multi-provider support in a single self-hosted deployment, no per-user fees, MIT licensed, and health intelligence built in: normalized recovery, sleep, activity and biometric data ready for your product layer.

We offer custom deployment and integration support through Momentum. If your team needs help getting to production faster, we can set up and configure Open Wearables as part of a managed engagement. Let's talk.

Book a demo to see how Open Wearables fits your use case.

Run it in your own cloud

Open Wearables is open source and self-hosted. Start from the repository, or bring us in to stand it up in your account.