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
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
Steps
last 14 days
Oct 11: 9,412 steps
Sleep phases
last night
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.
- Apple Health
- Health Connect
- Withings
- Oura
- Whoop
- Fitbit
- Polar
- Strava
- Suunto
- Ultrahuman
- Dexcom
- Omron
- Libre
- Garmin
- Coros
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
01Collect
Apple Health uploads from your app. Withings over OAuth, with scheduled pulls and push.
02Keep
Every payload lands on disk or S3 first and is never rewritten.
03Normalize
One model, one set of units. Copies removed, days totalled in local time.
04Serve
REST with an OpenAPI file, and an MCP server that can only read.
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.
In
Out
- readings in
- 30readings in
- copies dropped
- 8copies dropped
- unit out
- 1unit out
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}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 1hScoped 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.gzkept as it came
replay after a fixapple-health · part 2.ndjson.gzkept as it came
Replay anything
Raw payloads are never rewritten, so a mapper fix reaches every past upload.
- 1DELETE /v1/users/{id}202 with a job_id
- 2Reads return 404and the user's tokens stop working
- 3Connections revokedat each provider
- 4Raw files deletedfrom disk or S3
- 5Every row deletedthen the user
- 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
apicollectetlSplit 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.
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.