Open Health Sync
One open API for wearable and health data. On your own server.
Apple Health, Withings, Oura, WHOOP, Fitbit, Health Connect, Dexcom, OMRON and five more.
One clean model out, over REST, FHIR R4 and a read-only MCP server for AI agents.
Free, one Go binary plus Postgres, runs on a Raspberry Pi.
An open source, self-hosted alternative to hosted wearable APIs such as Terra.
docker pull ghcr.io/soma-intelligence/open-health-sync:0.1.0
Rather not run a server? The hosted beta is the same API with nothing to operate: a key in a minute, demo data on the first call.
Get started for free → · Sign in to the developer portal · ▶ Watch the 75-second tour
Quickstart · Hosted beta · Compare · Roadmap · Use cases · Why · Features · How it works · MCP · FAQ
If it saves you one more parser, a ⭐ star helps other developers find it. Questions and ideas: Discussions.
v0.1.0, early preview. APIs, configuration and schema may change in any 0.x release.
The quickstart, recorded on v0.1.0 with synthetic data: start the stack, load 90 days from six devices, then read one day of the merged view, one heart rate reading as REST, FHIR R4 and Open mHealth, and the MCP tools. Nothing is mocked; the tape renders it again.
Open Health Sync (ohs) is a self-hosted ingestion and normalization layer for health data. It
pulls from providers, accepts uploads from phones, stores each payload verbatim, maps it to one
canonical model with one set of units, and serves it over a REST API and a read-only MCP server.
It is not a dashboard or a coaching app, and it is not a medical device or clinical software.
Run it yourself, or let us run it for you in the hosted beta.
|
🔌 Thirteen sources, one model |
⚡ Always current |
🧾 Nothing thrown away |
|
🤖 Ready for AI agents |
🏥 Standards out |
🔒 Yours, and erasable |
Hosted beta
The same server, run for you. The hosted beta is Open Health Sync without a Postgres, a raw store or workers to operate: you sign in, create a key and read data on your first call.
A 75-second tour on test data: create an account and a project, connect a coding agent over MCP, load sample data, see it charted, call the API from the explorer, then Ctrl K. Play the video (MP4, 9 MB).
- A key in a minute. Create a project and get its API key. During the beta: three projects per account, five keys per project.
- Data on the first call. One click adds a synthetic user with 30 days from six sources, so your first request already returns sleep, heart rate and steps.
- Every output. REST with the OpenAPI file, FHIR R4, Open mHealth, CSV and Parquet, signed webhooks and the read-only MCP server, as documented here.
- A quickstart with your values. curl, TypeScript and Python filled in with your instance's address, plus the example apps and an AI agent over MCP.
- Nothing to migrate. It runs this open source server. Code written for it moves to your own instance with a new base URL and key, and back.
The web example reading a user back with a project key: daily steps, resting heart rate and sleep stages, all synthetic data.
It is a free beta with open sign-up, on shared capacity. For now it takes test data only: demo and synthetic users, no real people's health data. Self-hosting stays first class, and the server stays free under the AGPL.
Get started for free → Sign up with an email and confirm it. Already have an account? Sign in to the developer portal.
Roadmap at a glance
✅ in v0.1.0 · 🔜 on main, ships in the next release · 🛠 planned
| Outputs and integrations | Sources | ||
|---|---|---|---|
| ✅ | REST API (OpenAPI 3.1), read-only MCP server | ✅ | Apple Health, Withings |
| ✅ | FHIR R4 (Observations, checked with the HL7 validator) | ✅ | Oura, WHOOP, Polar, Strava |
| ✅ | Open mHealth output (docs) | ✅ | Fitbit, Suunto, Ultrahuman, intervals.icu |
| ✅ | CSV and Parquet exports (docs) | ✅ | Dexcom (CGM), OMRON |
| ✅ | Outgoing webhooks (docs) | ✅ | Health Connect (Android app upload) |
| ✅ | Background sync and /freshness | ✅ | Health app export import |
| ✅ | Prometheus metrics and OpenTelemetry traces, off by default (docs) | ||
| ✅ | A merged view across sources, with priorities per user and metric (docs) | ||
| ✅ | Admin console at /console: run the pipeline and turn providers on from the browser () |
A line moves up only when its release is published. Details per source: docs/connectors.
What you can build
- A health app backend. Your iOS app uploads HealthKit data, your users connect their Oura, WHOOP or Withings accounts (through a hosted connect page with your logo), and your app reads one API instead of thirteen.
- An AI agent over your own data. Run it at home, connect your devices, and give your desktop agent a read-only view of your sleep, workouts and heart rate through MCP.
- A study that collects from participants' wearables. Raw payloads kept for audit, hard deletes for withdrawals, FHIR R4 out for the analysis side.
- A personal data archive. Load years of history from the Health app export in one command, and keep it on a machine you own.
- Small tools on your own data.
ohs admin personal setupgives you one instance for yourself, and the recipes add a sleep report, a heart trend, a weekly digest, a dashboard and more, each a single Python file with no dependency.
Why
Every health source speaks its own dialect. Apple Health sends one JSON line per HealthKit
sample, heart rate in count/min, oxygen saturation as a fraction. Withings sends a daily
activity object keyed by date, and intraday series keyed by Unix epoch. Each one has its own
idea of a day, a device and a duplicate.
So teams write a script per provider: a parser, a table, a cron job. 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.
Open Health Sync is that layer, written once, with the boring parts done properly.
| DIY script per provider | Open Health Sync | |
|---|---|---|
| Add a source | a new parser, new tables, a new cron job | a connector that maps into the same model |
| Units | whatever each API sends | one unit per quantity, the original value and unit kept in metadata |
| "Today" | UTC, or the provider's clock | the local calendar day of the record's time zone |
| A re-sent upload | stored twice | answered 200 with "duplicate": true, records de-duplicated by content hash |
| Raw payloads | parsed, then thrown away | kept verbatim on disk or S3, so a user can be re-processed after a fix |
| Is it done? | guess, or check the logs | GET /coverage: pending_jobs, parked_jobs, rejected_records |
| Is it current? | a cron job and hope | webhook-triggered pulls, a backstop pull, GET /freshness per source |
| A record deleted at the provider | stays in your tables | deleted here too, when the provider reports it |
| New data arrived | poll on a timer | a signed webhook (data.written) names the user, provider and days |
| A crash mid-run | rerun the script and hope | Postgres queue, retries with backoff, parked after 5 attempts |
| Provider OAuth tokens | a plain column or a .env | sealed with AES-256-GCM under OHS_ENCRYPTION_KEY |
| Delete a user | grep across tables and buckets | DELETE /v1/users/{id}, then poll GET /v1/deletions/{job_id} |
| Erase one provider's data | the same grep, filtered by hand |
Features
- One canonical model. Quantity samples, category samples, sleep sessions and phases,
workouts and routes, nutrition, daily summaries. Each record carries its provenance:
provider,source,deviceandingested_at. - One unit per quantity. Quantity names come with UCUM units and LOINC codes
(data model). Heart rate is
bpmwhatever the source called it. - Raw payloads are kept. Every upload and pull lands in the raw store (local folder or any S3 compatible bucket) before anything else, and is never rewritten. After a mapper fix, any user can be re-processed from the raw files (how).
- Local days, not UTC days. Daily summaries are per local calendar day, from the record's time zone. When a provider sends no daily summary, step, energy and distance totals are derived from samples.
- Idempotent uploads. Each Apple Health part is stored under its day, batch and part
number. Re-sending the same bytes answers
200with"duplicate": true; other bytes under a taken key are refused, never written over. Records are de-duplicated by content hash. - Always current, without a sync button. Provider webhooks trigger a pull of just what
changed, ahead of any routine work; a backstop pull every 6 hours (hourly when webhooks are
silent) catches anything lost.
GET /v1/users/{id}/freshnesssays how up to date each source is (background sync). - Know when it's processed. The coverage report counts records per provider and local day, plus jobs still pending, jobs parked after their retries, and lines that were not stored.
- Real erasure.
DELETE /v1/users/{id}removes the user's raw files and every row, then the user. Not a soft flag. When a provider reports that a record was deleted (Oura, WHOOP, Strava), it is deleted here too. Disconnecting a source with?delete_data=trueerases everything that source sent for the user (records, raw files, sync history), and does the same for Apple Health or Health Connect. drops raw files and normalized data past an age.
Sources and outputs
Thirteen sources are in v0.1.0. Garmin, Eight Sleep, Xiaomi, and Amazfit and RingConn through Health Connect, are on main for the next release.
Every other one is planned: on the roadmap, not built yet.
Integration paths, vendor programs and tracking issues for each:
docs/connectors.
| Source | Status | How the data arrives |
|---|---|---|
| Apple Health | ✅ Available | your iOS app uploads HealthKit data with the Swift SDK or its own code (contract); ohs import apple-health loads a Health app export |
| Withings | ✅ Available | OAuth, scheduled pulls, push notifications (guide) |
| Oura | ✅ Available | OAuth, scheduled pulls, webhooks (guide) |
| Whoop | ✅ Available | OAuth, scheduled pulls, webhooks (guide) |
| Strava | ✅ Available | OAuth, scheduled pulls, webhook events (guide) |
| Polar | ✅ Available | OAuth, scheduled pulls, webhooks (guide) |
| Garmin | 🔜 Next release | partner program: OAuth with PKCE, scheduled pulls, Garmin's notifications; bring your own Garmin app, and Garmin has paused new applications (guide) |
Want one sooner? Connectors are self-contained: read writing a connector and pick a good first issue.
| Output | Status |
|---|---|
| REST JSON, described by api/openapi.yaml | ✅ Available |
| MCP server, read only (docs/mcp.md) | ✅ Available |
| FHIR R4, checked with the official HL7 validator (docs/fhir.md) | ✅ Available (Observations, Patient, Device, Provenance, Patient $everything) |
| RR intervals and HRV (RMSSD, SDNN, pNN50, mean RR and heart rate) per heartbeat series, over REST and MCP (docs/hrv.md) | ✅ Available |
| Menstrual cycles derived from recorded flow, over REST and MCP; no predictions (docs/menstrual-cycles.md) | ✅ Available |
| Time tags (illness, injury, travel, jet lag, vaccination, medication, alcohol) that give context to the data, over REST, webhooks and MCP (docs/time-tags.md) | ✅ Available |
| Read routes for ECG recordings, which are stored today | 🛠 Planned |
FHIR R4 bulk export ($export) | 🛠 Planned |
| Open mHealth (docs/open-mhealth.md) | ✅ Available |
| CSV and Parquet exports () |
Quickstart
You need Docker with Compose v2.24 or later, plus curl, jq and openssl. Compose pulls the
published image ghcr.io/soma-intelligence/open-health-sync (amd64 and arm64).
git clone https://github.com/soma-intelligence/open-health-sync.git && cd open-health-sync
cp .env.example .env
sed -i.bak "s|^OHS_ENCRYPTION_KEY=.*|OHS_ENCRYPTION_KEY=$(openssl rand -base64 32)|" .env
docker compose --env-file .env -f deploy/docker-compose.yml up -d --wait
docker compose --env-file .env -f deploy/docker-compose.yml run --rm ohs doctor
docker compose --env-file .env -f deploy/docker-compose.yml exec ohs /ohs admin tenant create --name dev
ohs doctor prints one ok or fail line per check (configuration, database, raw store,
public URL, connectors); run it again whenever you change .env. The last command prints a tenant API key once. Keep it:
export OHS=http://localhost:8080
export KEY=ohs_sk_... # the key printed above
The API listens on 127.0.0.1 only, so these steps run on the same machine. A phone app,
provider webhooks or another computer need it exposed: put a reverse proxy with TLS in front, or
set OHS_HTTP_BIND=0.0.0.0 in .env for plain http on your network.
No device or provider keys yet? ohs seed --demo fills the instance with 90 days of synthetic
data from six overlapping sources and prints a demo API key and calls to try
(docs/demo.md):
docker compose --env-file .env -f deploy/docker-compose.yml exec ohs /ohs seed --demo
Port 8080 taken? Set OHS_HTTP_PORT and a matching OHS_PUBLIC_URL in .env (see
self-hosting).
Create an end user and a token the user's app would hold:
USER_ID=$(curl -s -X POST $OHS/v1/users -H "Authorization: Bearer $KEY" \
-H 'Content-Type: application/json' -d '{"external_ref":"demo"}' | jq -r .id)
TOKEN=$(curl -s -X POST $OHS/v1/users/$USER_ID/tokens -H "Authorization: Bearer $KEY" \
-H 'Content-Type: application/json' -d '{"scopes":["upload"]}' | jq -r .token)
Upload one Apple Health part (one heart rate reading, gzip compressed):
DAY=$(date -u +%Y-%m-%d); NOW=$(date -u +%Y-%m-%dT%H:%M:%SZ)
printf '{"quantityType":"HKQuantityTypeIdentifierHeartRate","startDate":"%s","endDate":"%s","value":72,"unit":"count/min","sourceName":"Demo Watch","metadata":{"HKTimeZone":"UTC"}}\n' "$NOW" "$NOW" \
| gzip | curl -s -X POST $OHS/v1/ingest/apple-health \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/gzip" \
-H "X-OHS-Type: HKQuantityTypeIdentifierHeartRate" -H "X-OHS-Day: $DAY" \
-H "X-OHS-Batch: demo1" -H "X-OHS-Part: 0" --data-binary @-
Wait until processing is done (pending_jobs is 0), then read the normalized sample back:
curl -s "$OHS/v1/users/$USER_ID/coverage" -H "Authorization: Bearer $KEY"
curl -s "$OHS/v1/users/$USER_ID/samples?type=heart_rate" -H "Authorization: Bearer $KEY"
Install the ohs binary on your machine. The import below and the MCP setup
run ohs outside the container. Take the archive for your system from the
releases page
(ohs_<version>_<os>_<arch>.tar.gz, os is linux or darwin, arch is amd64 or
arm64); it unpacks to a folder of the same name:
tar xzf ohs_*_linux_amd64.tar.gz
sudo install ohs_*_linux_amd64/ohs /usr/local/bin/ohs
ohs version
On macOS, clear the quarantine flag first: xattr -d com.apple.quarantine ohs_*_darwin_*/ohs.
Load your existing history.
On the iPhone, Health app, your picture, Export All Health Data gives export.zip. Load it
with the user's token (--dry-run first counts what it would send):
OHS_TOKEN=$TOKEN ohs import apple-health export.zip --url $OHS
Records, workouts with their routes and ECGs go through the same upload API, in constant memory. Running it again uploads nothing twice. Details: importing an export.
Data lives in the pgdata and raw volumes; docker compose ... down keeps them.
How it works
flowchart LR
AH["Apple Health, Health Connect<br/>app upload, export import"] --> C
CL["Withings, Oura, WHOOP, Fitbit,<br/>Polar, Strava, Suunto, Ultrahuman,<br/>intervals.icu, Dexcom, OMRON<br/>OAuth pull + webhooks"] --> C
C["collect / ingest"] --> R[("raw store<br/>disk or S3")]
R --> E["ETL<br/>normalize, dedup,<br/>units, local-day summaries"]
E --> P[("Postgres<br/>+ TimescaleDB")]
P --> API["REST API<br/>(OpenAPI)"]
P --> MCP["MCP server<br/>(read only)"]
P --> FHIR["FHIR R4<br/>Observations"]
- Collect. Your iOS app posts HealthKit data to
POST /v1/ingest/apple-healthas gzip NDJSON parts, with a user token that has theuploadscope and the headersX-OHS-Type,X-OHS-Day,X-OHS-BatchandX-OHS-Part. The cloud providers (Withings, Oura, WHOOP, Fitbit and the others) are pulled when they send a webhook (only what changed), with a backstop pull on a schedule. - Keep the raw payload. The part is written to the raw store first, then a job is queued in Postgres. The answer names the raw key and whether the part was a duplicate.
- Normalize. The
etlrole maps each line to the canonical model, converts units, drops duplicates, attributes source and device, and rolls up daily summaries per local day. - Serve. Reads are
GET /v1/users/{id}/...with the tenant key, paged with a cursor. The MCP server reads the same data, read only.
Details: architecture, API guide.
A provider is turned on, then a user connects
In the admin console you paste the keys of your Oura app and Oura is on, with no restart. Your
app then creates a connection link and sends its user to it. The user picks Oura, then Withings,
and allows access at each. ohs exchanges the code, stores the grant encrypted, pulls the history
in the background and normalizes it. Back in the console, the pipeline shows the data moving
through and the user's page shows each source active and fresh.
Recorded on this repository with synthetic data. The console, the connect page, the code exchange, the encrypted grant, the background pulls and the normalization are the real ones; the provider side (consent screen and API) is played by the test servers of the connector tests. How it was made: docs/demo.
See it run
These are real captures, not mockups: ohs 0.0.1 built from this repository, run natively
against TimescaleDB (on port 8088, since 8080 was taken on that machine), with the end to end
Apple Health fixture in connectors/applehealth/testdata/e2e
uploaded as six parts. The fixture's values are synthetic.
A sample comes back in canonical units, with what the provider sent kept in metadata
(count/min became bpm):
Daily summaries are derived per local day, and the coverage report says when processing is done:
Connect an AI agent (MCP)
Mint a user token with only the read scope (it may live up to 30 days):
curl -s -X POST $OHS/v1/users/$USER_ID/tokens -H "Authorization: Bearer $KEY" \
-H 'Content-Type: application/json' -d '{"scopes":["read"],"ttl_seconds":2592000}'
Then add ohs mcp (stdio) to your desktop agent's mcpServers (the binary from
the install step):
{
"mcpServers": {
"open-health-sync": {
"command": "/usr/local/bin/ohs",
"args": ["mcp"],
"env": { "OHS_MCP_URL": "http://localhost:8080", "OHS_MCP_KEY": "ohs_ut_..." }
}
}
}
ohs mcp holds no data and needs no database: it forwards each message to the /mcp endpoint
of an instance. OHS_MCP_URL must be https unless the host is loopback. Agents that speak
Streamable HTTP can call /mcp directly with the bearer token.
On main for the next release, the server also lists twelve prompts (prompts/list): ready-made
questions about training, sleep and recovery that read the data first and show the numbers
(prompts, prompt library). The same guidance
ships as an agent skill in skills/open-health-sync.
Tools, arguments and paging: docs/mcp.md.
Data model
Every source maps into these records (see docs/data-model.md for fields and units):
- QuantitySample: one scalar measurement, such as heart rate, steps or SpO2.
- CategorySample: a categorical event, such as a stand hour or a menstrual flow entry.
Menstrual cycles are derived from the flow entries at read time by
/menstrual-cycles(docs/menstrual-cycles.md). - SleepSession and SleepPhase: a night with its duration breakdown, then each stage.
- Workout and RoutePoint: a session with its metrics, then its GPS track.
- NutritionLog: a food or drink entry with its nutrients.
- ECGRecording and ECGVoltageSample: the recording's metadata, then the voltage trace. Stored, not served yet: the read route is planned.
- HeartbeatSample: one beat-to-beat (RR) interval. Served with HRV derived per series by
/rr-intervalsand/hrv(docs/hrv.md). - DailySummary: totals per local day, from the provider or rolled up from samples.
- Observation: a generic coded fact for the long tail. No connector writes it yet.
Deploy
- Compose with the published image.
deploy/docker-compose.yml, as in the quickstart. Pin the tag withOHS_VERSION(there is nolatestwhile 0.x). See self-hosting. - Raspberry Pi 5. Add the overlay
deploy/docker-compose.pi.yml(smaller Postgres buffers, memory limits). See Raspberry Pi 5. - S3 raw store. Add
deploy/docker-compose.s3.yml, or pointOHS_RAW_STOREat any S3 compatible bucket. See S3. - Backup and restore.
scripts/backup.shwrites one archive with the database, the raw store and the schema version;scripts/restore.shputs it back. CI destroys and restores a stack on every pull request. See backup and restore. - Release binaries. Linux and macOS, amd64 and arm64, with
checksums.txt, on the releases page. Bring your own Postgres with TimescaleDB and runohs doctor, thenohs up(migrate, then serve). Each archive unpacks toohs_<version>_<os>_<arch>/ohs(install). The macOS binaries are not signed yet: clear the quarantine flag withxattr -d com.apple.quarantine. From the next release, the image andchecksums.txtare signed with cosign (verifying releases). - Go 1.26: gives . For compose, add , which needs ; without it, see .
Open Health Sync vs Terra and other health data APIs
Looking for a Terra API alternative, or an open source health data aggregator? Here is how Open Health Sync compares with the hosted wearable APIs and the other open source option. Facts about other products come from their own sites and pricing pages, checked on 2026-10-17; each row links to a full comparison, with how to move.
| Open source | Starting price | FHIR R4 | MCP server for AI agents | Where the data lives | |
|---|---|---|---|---|---|
| Open Health Sync | Yes, AGPL-3.0 | Free, you pay for your server | Yes, checked with the official HL7 validator | Read-only, stdio and HTTP | Your server |
| Terra | No | $399 a month billed annually | A FHIR option, no version named | Read-only | Terra's cloud, then your destination |
| Open Wearables | Yes, MIT | Free, custom Enterprise tier | Not in its documentation | Yes, stdio | Your server |
| Junction | No | $300 a month for up to 500 users | Not in its documentation | Not in its documentation | Junction's cloud, US or EU |
| Thryve | No | 499 EUR a month for up to 500 users | Not found on its site | Not found on its site | Thryve's servers in Germany |
| Spike API | No | From $450 a month | Not found in its documentation | Yes | Spike's cloud |
| Sahha |
Pick a hosted API when you need Garmin or a long tail of devices today, do not want to run a server, or need your vendor's SOC 2 and HIPAA attestations. Pick Open Health Sync when the data has to stay on your infrastructure, per-user fees do not fit, or you need standard output (FHIR R4, Open mHealth) and every raw payload kept. Every option, side by side: health data aggregators compared.
FAQ
Who sees my data?
Only whoever runs the instance. Self-hosted, there is no account to create with us, no analytics and no call home. The server talks to the providers you enable (the ones whose keys you set) and to your own database and raw store. Logs never contain tokens, keys or health values. On the hosted beta we run the instance, which is why it takes test data only for now.
Where does the data live?
In two places you control: Postgres with TimescaleDB (connections and jobs in the ohs_sync
schema, normalized records in ohs_data) and the raw store, a local folder or an S3 compatible
bucket. With compose, those are the pgdata and raw volumes. Back them up together, with
OHS_ENCRYPTION_KEY.
Is it HIPAA or GDPR compliant?
No software is compliant on its own, and Open Health Sync is not certified, audited or covered
by a business associate agreement. What it gives you: data stays on infrastructure you choose,
DELETE /v1/users/{id} is a hard delete of raw files and rows (useful for erasure requests),
erasing one source's data for a user is a single call,
consent is kept per source and data category and enforced on every write,
GET /v1/users/{id}/archive gives one person everything held about them in a zip,
OHS_RETENTION_DAYS enforces a retention period, every query is scoped to a tenant and a user,
keys and tokens are stored hashed, and provider OAuth tokens are encrypted with AES-256-GCM
under a key that can be rotated online (next release, key rotation).
Health records themselves are not encrypted by ohs: use disk or database encryption, TLS in
front of the API, and your own access controls. Whether a deployment meets HIPAA, GDPR or any
other rule depends on how you run it.
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. Each cloud provider (Withings, Oura, WHOOP, Fitbit and the others) needs your own developer application, registered with the vendor. The hosted beta is free while it is a beta.
Is there a hosted version?
Yes, in free beta with open sign-up: the same API, run for you on shared capacity, for test and demo data only for now. Sign up free; the hosted beta section says what it gives you.
How does it compare to Terra or Open Wearables?
Terra is a hosted API: it advertises 500+ integrations, Garmin included, starts at $399 a month billed annually, and your users' data passes through its cloud. Open Wearables is open source (MIT) and self-hosted like Open Health Sync, with a dashboard and a larger community. Open Health Sync is free and puts standards first: FHIR R4 and Open mHealth output checked against the official validators, every raw payload kept, and one Go binary that runs on a Raspberry Pi. See the comparison table, or side by side: Terra API alternative, Open Health Sync vs Open Wearables.
Which devices does it work with?
Anything that writes to Apple Health (Apple Watch, iPhone, and the apps and devices that sync
into HealthKit), through an iOS app that uploads it: the Swift SDK reads
HealthKit and uploads what changed, or your app implements the
upload contract itself. Without an app, ohs import apple-health
loads the export the Health app writes, history included. Withings scales, watches and sleep devices
through the user's Withings account. Oura rings, WHOOP bands, Polar watches, Strava
activities, Suunto watches, Fitbit devices (through the Google Health API), Ultrahuman rings,
intervals.icu accounts, Dexcom CGMs and OMRON blood pressure monitors and scales work through
the user's own account with each. Fitbit, Suunto, Ultrahuman and OMRON need keys their vendor gives to approved
developers only. On Android, anything that writes to Health Connect (Samsung Health, CGM apps
such as FreeStyle Libre) arrives through an app that uploads it
(contract). Garmin watches work through the user's Garmin
Connect account on main for the next release, with keys Garmin gives approved businesses only
(guide). More sources are
planned, and the
connector request form
is open.
Why AGPL?
So the layer stays open. Anyone can run, study and change it; if you run a modified Open Health Sync as a network service, you publish your changes under the same license. The client SDKs in sdks/ are AGPL-3.0-only too.
Can I use it commercially?
Yes. A company can run it, unmodified or modified, to power its own product. The AGPL only asks that users who interact with a modified server over the network can get its source. This is not legal advice: read LICENSE or ask your counsel.
Is it a medical device?
No. It moves and normalizes data. It does not diagnose, score or advise, and it is not clinical software.
Status and roadmap
v0.1.0 is the latest release. It adds the Oura, WHOOP, Strava, Polar, Fitbit, Suunto, Ultrahuman, intervals.icu, Dexcom, OMRON and Health Connect connectors, FHIR R4 and Open mHealth output, CSV and Parquet exports, outgoing webhooks, Health app export import, background sync, sample compression, RR intervals and HRV, menstrual cycles, time tags, per-source erasure, a merged view across sources, backup and restore, the hosted connect page, sync history and connection health, personal mode with its recipes, an audit log, consent per source and category, a complete user archive, a read-only admin console and a Helm chart for Kubernetes. What changed: CHANGELOG.md. Install and upgrade notes and the full list of limitations: release notes.
Known limitations:
- After a hard kill, a job that was in flight is picked up again only after its 10 minute visibility timeout.
- One database and one raw store per instance; no multi-region replication.
- FHIR R4 has no bulk export (
$export) yet. - The client SDKs are 0.1.0 and not on package registries yet; install them from this repository. Reading Health Connect from the Kotlin SDK is planned.
The hosted beta is a free beta with open sign-up (shared capacity, test and demo data only): sign up free.
Next: more connectors, standard outputs (more FHIR R4 resources), then Health Connect reading in the Kotlin SDK, then health records (EHR) through SMART on FHIR. Requests are welcome as GitHub issues, including the connector request form.
Documentation
- Self-hosting: configuration, images, Raspberry Pi, S3, operating notes.
- Security model and hardening checklist: what is protected and how, and what to set before an instance holds real people's data.
- API guide and the OpenAPI file, the source of truth.
- MCP server: tools, prompts, credentials, desktop and remote agents.
- FHIR R4 output: the user's records as Observations, the mapping and its limits.
- Outgoing webhooks: events, signatures, retries and the endpoint routes.
- Data model: every canonical type and quantity name, with units.
- Client SDKs: Kotlin, , , .
Contributing
Issues and pull requests are welcome. Three ways in, from five minutes to a weekend:
- Own a wearable? Record your account in about five minutes with
make record PROVIDER=oura(or any other source). The recorder anonymizes on your machine and CI replays the file on every change: pick a source in #60, and see recording a connector. - Pick a good first issue: docs, a recipe, a CLI command or a deploy template, each scoped to one PR.
- Add a provider. Connectors are self-contained: start with writing a connector.
Read CONTRIBUTING.md and the code of conduct first. Every first-time contributor gets a review within 48 hours.
make setup # git hooks
make build # bin/ohs
make test # unit tests
make integration # needs Docker (testcontainers)
Contributors
Security
Report vulnerabilities privately, as described in SECURITY.md. Never paste health data, keys or tokens in a public issue.
License
The server is AGPL-3.0-only, see LICENSE. If you run a modified Open Health Sync as a service, you publish your changes. The client SDKs in sdks/ are under the same license.