Version 0.1.0, an early preview. Free under AGPL-3.0 (opens in a new tab)

Your health data layer, already built.

Collect from devices and apps, keep every raw payload and serve one clean model over REST and MCP. One Go binary plus Postgres, on your own server.

Go binary, plus Postgres
1Go binary, plus Postgres
quantity types with UCUM units
70+quantity types with UCUM units
read-only MCP tools
11read-only MCP tools
is enough, on a Raspberry Pi 5
4 GBis enough, on a Raspberry Pi 5
Or pull the image
Your app, on clean dataExample data

Resting heart rate

54bpm

resting_heart_rate · LOINC 40443-4

Asleep last night

7h 20m

SleepSession and its sleep_phases

Steps today

9,412

DailySummary for the local day

Heart rate

heart_rate · bpm · every 15 min

6010014000:0006:0012:0018:00Run · 166 bpm

Steps

last 14 days

5k10kTodaySep 28

Oct 11: 9,412 steps

Sleep phases

last night

Awake
REM
Core
Deep
23:1007:05
An app you could build on the API, drawn from made-up data. Open Health Sync serves the numbers. The charts are yours.

The part every health app rebuilds.

Health data arrives in different units, sometimes twice and in the wrong time zone. Open Health Sync deals with that on your own server, so your app starts from clean records.

Every lit source ships in 0.1.0, with intervals.icu. Libre readings arrive through Health Connect or Apple Health. Garmin and Coros, faded, are planned. See every integration.

What it reads, in one model

  • Heart rateheart_rate · /min
  • Heart rate variabilityhrv_sdnn · ms
  • Sleepsessions and phases
  • Stepsstep_count
  • Workoutswith routes
  • Blood oxygenoxygen_saturation · %
  • Body weightbody_mass · kg
  • Blood glucoseblood_glucose · mg/dL
  • Blood pressuresystolic, diastolic · mm[Hg]
  • Temperaturebody_temperature · Cel
  • Respiratory raterespiratory_rate · /min
  • Nutritionnutrition logs

The script you’d write, done properly.

Teams write a script per provider. It works until the second provider, the first re-sent upload, the first mapper bug that needs a backfill, or the first person who asks you to delete everything you hold about them.

Add a source
A script per provider: A new parser, new tables, a new cron job
Open Health Sync: A connector that maps into the same model
Units
A script per provider: Whatever each API sends
Open Health Sync: One unit per quantity, the original kept in metadata
“Today”
A script per provider: UTC, or the provider’s clock
Open Health Sync: The local calendar day of the record’s time zone
A re-sent upload
A script per provider: Stored twice
Open Health Sync: Answered as a duplicate, records de-duplicated by hash
Raw payloads
A script per provider: Parsed, then thrown away
Open Health Sync: Kept verbatim, so a user can be re-processed after a fix
A crash mid-run
A script per provider: Rerun the script and hope
Open Health Sync: A Postgres queue, retries with backoff
Delete a user
A script per provider: Grep across tables and buckets
Open Health Sync: One call, then poll until it’s done
Let an AI agent read it
A script per provider: Build an integration
Open Health Sync: A read-only MCP server

One binary from device to database.

The job queue lives in Postgres too, so there's no broker, cache or worker fleet to run next to it.

In

  • Apple Health
  • Withings
  • Oura, WHOOP, Fitbit and 8 more

Your server

  • Raw storeDisk or S3, never rewritten
  • NormalizeUnits, dedup, daily totals
  • PostgresWith TimescaleDB

Out

  • REST API
  • MCP, read only
  • FHIR R4, webhooks, exports
Data from Apple Health, Withings, Oura, WHOOP, Fitbit and eight more sources goes into a raw store on your own server, kept as it came. It's then normalized into Postgres, served over a REST API, a read-only MCP server, FHIR R4 and Open mHealth, with CSV and Parquet exports and outgoing webhooks.
  1. 01Collect

    Apple Health uploads from your app. Withings over OAuth, with scheduled pulls and push.

  2. 02Keep

    Every payload lands on disk or S3 first and is never rewritten.

  3. 03Normalize

    One model, one set of units. Copies removed, days totalled in local time.

  4. 04Serve

    REST with an OpenAPI file, and an MCP server that can only read.

  5. 05Erase

    One call removes a user's raw files and rows, then the user.

Two sources and two units become one series.

Each reading maps to one quantity and one unit, copies are dropped, and every point keeps the provider, source and device it came from.

Apple HealthWithingsSame reading sent twiceOne hour, made-up data

In

Apple Health · count/minWithings · bpm
ohs etl

Out

heart_rate · bpm · UCUM /min08:0008:3009:00
readings in
30readings in
copies dropped
8copies dropped
unit out
1unit out
heart_rateUCUM /minLOINC 8867-4
resting_heart_rateUCUM /minLOINC 40443-4

Plain HTTP in, clean JSON out.

Any language that can make a request can read it. Lists page with a cursor, and the OpenAPI file (opens in a new tab) generates a client if you'd rather not write one. Every route is in the API reference.

curl "$OHS/v1/users/$USER_ID/samples?type=heart_rate" \  -H "Authorization: Bearer $KEY"

200 OKapplication/json

{"data": [{"type": "heart_rate","value": 72,"unit": "bpm","start_date": "2026-10-11T08:14:00Z","provider": "apple_health","device": "Apple Watch"}],"next_cursor": null}
Reading a user's heart rate samples from the REST API in cURL, and the JSON it returns.

Let an agent read, and only read.

The MCP server speaks stdio for desktop agents and Streamable HTTP for remote ones. Give it a token with the read scope and it can answer questions about sleep, workouts and heart rate without changing a thing.

mcpServers

{
  "open-health-sync": {
    "command": "/usr/local/bin/ohs",
    "args": ["mcp"],
    "env": {
      "OHS_MCP_URL": "https://ohs.example.org",
      "OHS_MCP_KEY": "ohs_ut_..."
    }
  }
}

Tools the agent gets

  • list_users
  • list_quantity_types
  • get_samples
  • get_category_samples
  • get_sleep
  • get_workouts
  • get_workout_route
  • get_nutrition
  • get_daily
  • get_coverage
  • list_connections

Ready for real people's data.

Scoped tokens, deletion that deletes, raw files you can replay and the operational basics, so it runs for users and not only on a laptop.

  • Tenant key

    ohs_sk_••••••••••••

    Lives on your server. Users, tokens, connections and reads.

    User token

    ohs_ut_••••••••••••
    uploadconnectreadttl 1h

    Scoped tokens

    Tenant keys stay on your server. Apps get short-lived user tokens, scoped to upload, connect or read.

  • api/openapi.yaml

    /v1/users/{id}/samples:get:summary: Quantity samples of one typeparameters: [type, start, end, cursor]

    OpenAPI first

    One file is the source of truth. Generate a client in any language.

  • apple-health · part 0.ndjson.gz

    kept as it came

    apple-health · part 1.ndjson.gz

    kept as it came

    apple-health · part 2.ndjson.gz

    kept as it came

    replay after a fix

    Replay anything

    Raw payloads are never rewritten, so a mapper fix reaches every past upload.

    1. 1DELETE /v1/users/{id}202 with a job_id
    2. 2Reads return 404and the user's tokens stop working
    3. 3Connections revokedat each provider
    4. 4Raw files deletedfrom disk or S3
    5. 5Every row deletedthen the user
    6. GET /v1/deletions/{job_id}says it's done

    Deleting really deletes

    One call, then a background job removes everything the user ever sent. Poll it until it's done.

  • GET /metricsexample

    Prometheus metrics

    Counters on /metrics, ready for the dashboards you already run.

  • apicollectetl

    one process, or

    apicollectetl

    Split roles

    api, collect and etl run in one process, or apart when one of them needs to scale.

  • Sealed OAuth tokens

    Provider tokens are encrypted at rest with your own 32-byte key, which never leaves your server.

  • ohs doctor

    Checks your configuration in one command, and prints an encryption key if you need one.

  • Safe retries

    Idempotency keys on user creation, and uploads keyed by batch and part.

  • Docs for every part

    Self-hosting, the API, the MCP server, the data model and writing a connector. Read the docs (opens in a new tab)

Running in five commands.

You need Docker with Compose v2.24 or later, plus curl, jq and openssl. The last command prints your API key, and the quickstart takes you on to a first upload. No wearable yet? ohs seed --demo loads 90 days of synthetic data from six sources to try the API on.

Terminal
git clone https://github.com/soma-intelligence/open-health-sync.git && cd open-health-synccp .env.example .envsed -i.bak "s|^OHS_ENCRYPTION_KEY=.*|OHS_ENCRYPTION_KEY=$(openssl rand -base64 32)|" .envdocker compose --env-file .env -f deploy/docker-compose.yml up -d --waitdocker compose --env-file .env -f deploy/docker-compose.yml exec ohs /ohs admin tenant create --name dev
Docker Compose
Published image on GHCR, amd64 and arm64
Kubernetes via Helm
A chart with a migration Job, upgraded in CI
Render, Fly.io, Railway
Templates that build it and add the database
Release binaries
Linux and macOS, amd64 and arm64, or make build
Raspberry Pi 5
A compose overlay tuned for 4 GB
Your own Postgres
With TimescaleDB, raw store on disk or S3

What ships in 0.1.0, and what comes next.

Sources in 0.1.0

  • Apple Health and Health Connect uploads from your app
  • Oura, Whoop, Fitbit, Polar and Strava over OAuth
  • Withings, Suunto, Ultrahuman and intervals.icu
  • Dexcom CGM glucose and Omron blood pressure
  • Health app export import, with history
  • Background sync with a freshness report
  • Personal mode and eight small Python tools for your own data
  • A read-only admin console in the browser

Outputs in 0.1.0

  • REST API described by OpenAPI
  • Read-only MCP server, stdio and HTTP
  • FHIR R4 Observations, checked with the HL7 validator
  • Open mHealth, CSV and Parquet exports
  • Signed outgoing webhooks
  • A merged view across sources, with priorities per user
  • A read-only admin console
  • An append-only audit log
  • Consent per source and category, revocable
  • A complete per-user archive
  • Kotlin, Swift, React Native and Flutter SDKs, in the repository (not on a package registry yet)
  • Raw store on disk or S3, hard delete
  • Docker image and binaries, amd64 and arm64
  • Helm chart for Kubernetes

Planned

  • Garmin and Coros
  • Health records (EHR) through SMART on FHIR

Before you run it.

Who sees the data?

Only whoever runs the instance. It’s self-hosted software, not a service: no account with us, no analytics and no call home. Logs never contain tokens, keys or health values.

What does it cost?

Nothing. The server is free and open source. You pay for wherever you run it, which can be a Raspberry Pi 5 with 4 GB.

Is it HIPAA or GDPR compliant?

No software is on its own, and it isn’t certified or audited. It gives you the parts: data on infrastructure you choose, hard deletes, a retention period, per-tenant scoping, hashed keys and encrypted provider tokens. Health records themselves aren’t encrypted by ohs, so use disk or database encryption and TLS.

Why AGPL, and can I use it commercially?

Yes, a company can run it to power its own product. The AGPL keeps the layer open: if you run a modified server as a network service, you publish your changes. The client SDKs are AGPL-3.0 too.

How ready is it?

Early. Version 0.1.0 ships thirteen sources and FHIR R4, Open mHealth, CSV and Parquet exports and webhooks. APIs, configuration and schema may change in any 0.x release, so pin the version you run.

Is it a medical device?

No. It moves and normalizes data. It doesn’t diagnose, score or advise, and it isn’t clinical software.

Help build it.

Bug reports, connectors and fixes to the docs are all welcome. Soma Labs wrote it for Soma, then pulled it out so other teams can skip this part. Soma will run on the same code.

Free to run, read and change.

AGPL-3.0. It's developer infrastructure and isn't a medical device.