<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Quentin Caron</title>
    <description>The latest articles on DEV Community by Quentin Caron (@quentincar0n).</description>
    <link>https://dev.to/quentincar0n</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4191347%2F0eba5efe-bdf4-41cb-8d82-96d20ca2b447.png</url>
      <title>DEV Community: Quentin Caron</title>
      <link>https://dev.to/quentincar0n</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/quentincar0n"/>
    <language>en</language>
    <item>
      <title>Why I built an open source alternative to Terra for wearable data</title>
      <dc:creator>Quentin Caron</dc:creator>
      <pubDate>Sun, 18 Oct 2026 02:10:50 +0000</pubDate>
      <link>https://dev.to/quentincar0n/why-i-built-an-open-source-alternative-to-terra-for-wearable-data-37b9</link>
      <guid>https://dev.to/quentincar0n/why-i-built-an-open-source-alternative-to-terra-for-wearable-data-37b9</guid>
      <description>&lt;p&gt;Every wearable speaks its own dialect. Apple Health sends one JSON line per HealthKit sample, with heart rate in &lt;code&gt;count/min&lt;/code&gt; and oxygen saturation as a fraction. Withings sends a daily activity object keyed by date and intraday series keyed by Unix epoch. Oura, WHOOP and Fitbit each have their own idea of what a day, a device and a duplicate are.&lt;/p&gt;

&lt;p&gt;So every health app I worked on ended up writing the same layer: a parser per provider, a table per provider, a cron job 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  The obvious answer is a hosted API
&lt;/h2&gt;

&lt;p&gt;There is a whole category of products that solve this for you. Terra is the best known: one API, one widget for your users to connect their devices, and data pushed to your webhook or database. It advertises more than 500 integrations, Garmin included, and states SOC 2 Type II, HIPAA and GDPR compliance. If you need Garmin today, or a long tail of devices under one contract, and you don't want to run a server, it is a very reasonable choice.&lt;/p&gt;

&lt;p&gt;It did not fit what I was building, for four reasons.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Price per connected user.&lt;/strong&gt; Terra's public pricing starts at $399 a month billed annually ($499 monthly) with 100,000 credits, and each source a user connects costs 200 credits a month. By my arithmetic that covers about 500 connections. A product with 2,000 users who each connect one source would pay around $2,000 a month at the listed starting prices, before any volume discount. That is fair for a funded product. It is a lot for a free tier, a research study or a side project.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The data passes through someone else.&lt;/strong&gt; With a hosted API, your users' health data lands in the vendor's cloud before it reaches you. Terra's end-user privacy policy says it is usually kept there for two days and at most six months. That is a short window, but for some projects any third party in the path is one too many.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Raw payloads.&lt;/strong&gt; I wanted every payload kept verbatim, on my side, forever. Mappers have bugs. When I fix one, I want to re-process a user's whole history from the original files, not from a normalized copy that already lost the detail.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Standards.&lt;/strong&gt; I wanted FHIR R4 that passes the official HL7 validator and Open mHealth data points, so the data can go to a clinical system or an analysis tool without another conversion.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I built instead
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/soma-intelligence/open-health-sync" rel="noopener noreferrer"&gt;Open Health Sync&lt;/a&gt; is an open source, self-hosted health data aggregator. It is one Go binary plus Postgres with TimescaleDB, and it does the boring parts once:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;13 sources:&lt;/strong&gt; Apple Health and Health Connect (uploaded by a phone app, or imported from the Health app export), plus Withings, Oura, WHOOP, Fitbit, Polar, Strava, Suunto, Ultrahuman, intervals.icu, Dexcom and OMRON over OAuth, with provider webhooks triggering pulls of just what changed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Raw first:&lt;/strong&gt; every payload is stored verbatim on disk or S3 before anything else happens, and re-sent uploads are de-duplicated by content hash.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One model, one unit per quantity:&lt;/strong&gt; UCUM units, LOINC codes, and "today" means the local calendar day of the record's time zone, not UTC.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One answer per question:&lt;/strong&gt; when a ring and a watch both record the same night, reads return one merged view, with priorities per user and metric, and the raw rows stay available.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Many ways out:&lt;/strong&gt; a REST API (OpenAPI 3.1), FHIR R4 Observations, Open mHealth, CSV and Parquet exports, signed webhooks, and a read-only MCP server so an AI agent can read one person's data with a token that cannot write.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Erasable:&lt;/strong&gt; &lt;code&gt;DELETE /v1/users/{id}&lt;/code&gt; removes the rows and the raw files.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It idles at about 22 MB of memory and runs on a Raspberry Pi 5.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it in a few minutes
&lt;/h2&gt;

&lt;p&gt;You need Docker with Compose, plus &lt;code&gt;curl&lt;/code&gt;, &lt;code&gt;jq&lt;/code&gt; and &lt;code&gt;openssl&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/soma-intelligence/open-health-sync.git &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;open-health-sync
&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env
&lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt;.bak &lt;span class="s2"&gt;"s|^OHS_ENCRYPTION_KEY=.*|OHS_ENCRYPTION_KEY=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;openssl rand &lt;span class="nt"&gt;-base64&lt;/span&gt; 32&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;|"&lt;/span&gt; .env
docker compose &lt;span class="nt"&gt;--env-file&lt;/span&gt; .env &lt;span class="nt"&gt;-f&lt;/span&gt; deploy/docker-compose.yml up &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--wait&lt;/span&gt;
docker compose &lt;span class="nt"&gt;--env-file&lt;/span&gt; .env &lt;span class="nt"&gt;-f&lt;/span&gt; deploy/docker-compose.yml run &lt;span class="nt"&gt;--rm&lt;/span&gt; ohs doctor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No wearable at hand? This loads 90 days of synthetic data from six overlapping sources and prints an API key with calls to try:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose &lt;span class="nt"&gt;--env-file&lt;/span&gt; .env &lt;span class="nt"&gt;-f&lt;/span&gt; deploy/docker-compose.yml &lt;span class="nb"&gt;exec &lt;/span&gt;ohs /ohs seed &lt;span class="nt"&gt;--demo&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It prints the demo user's id and an API key once. Export them, then read one merged day, or the same heart rate data as FHIR:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;OHS_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://localhost:8080 &lt;span class="nv"&gt;OHS_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ohs_sk_... &lt;span class="nv"&gt;USER_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;...   &lt;span class="c"&gt;# values printed above&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$OHS_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$OHS_URL&lt;/span&gt;&lt;span class="s2"&gt;/v1/users/&lt;/span&gt;&lt;span class="nv"&gt;$USER_ID&lt;/span&gt;&lt;span class="s2"&gt;/daily"&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$OHS_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$OHS_URL&lt;/span&gt;&lt;span class="s2"&gt;/v1/users/&lt;/span&gt;&lt;span class="nv"&gt;$USER_ID&lt;/span&gt;&lt;span class="s2"&gt;/fhir/Observation?code=http://loinc.org%7C8867-4&amp;amp;limit=5"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your own Apple Health history loads from the export the Health app writes on your iPhone (Health app, your picture, Export All Health Data), with the &lt;code&gt;ohs&lt;/code&gt; binary from the releases page and a user token (the README shows how to create one):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;OHS_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$TOKEN&lt;/span&gt; ohs import apple-health export.zip &lt;span class="nt"&gt;--url&lt;/span&gt; &lt;span class="nv"&gt;$OHS_URL&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It reads the zip as a stream, so memory stays flat whatever the size of the export, and running it twice uploads nothing twice.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you give up
&lt;/h2&gt;

&lt;p&gt;Self-hosting is not free of trade-offs, and I would rather say them up front:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You register your own developer app&lt;/strong&gt; with Oura, WHOOP, Withings and the others. A hosted API does that part for you. Each connector guide lists the steps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;13 sources, not 500.&lt;/strong&gt; Garmin is not available.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No certification.&lt;/strong&gt; There are building blocks for a compliant setup (consent per source and data category, an append-only audit log, hard deletes, a per-user archive), but compliance depends on how and where you run it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AGPL-3.0.&lt;/strong&gt; Using it unmodified, or calling it from closed source apps over HTTP, is fine. If you run a modified version as a service, you share your changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Early days.&lt;/strong&gt; It is v0.1.0, and the API may change in any 0.x release.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  It is not the only open source option
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://openwearables.io" rel="noopener noreferrer"&gt;Open Wearables&lt;/a&gt; solves the same problem and came first. It has the larger community, a web dashboard, Garmin, and an MIT license, which matters if AGPL is a problem for you. I benchmarked both on the same machine with the same Apple Health data: Open Health Sync stored the records 2 to 3 times faster end to end and used about 22 MB of memory idle against about 494 MB, while Open Wearables used 3.4 times less disk. Those are numbers for one user, one machine and synthetic data, and the scripts are in the repository for anyone to run again.&lt;/p&gt;

&lt;p&gt;I wrote two comparison pages that try to be fair, including when the other option is the better pick:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.somalabs.me/open-health-sync/documentation/compare/terra" rel="noopener noreferrer"&gt;Open Health Sync and Terra&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.somalabs.me/open-health-sync/documentation/compare/open-wearables" rel="noopener noreferrer"&gt;Open Health Sync and Open Wearables&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Feedback welcome
&lt;/h2&gt;

&lt;p&gt;The code is at &lt;a href="https://github.com/soma-intelligence/open-health-sync" rel="noopener noreferrer"&gt;github.com/soma-intelligence/open-health-sync&lt;/a&gt;. I would love to hear which source you need next, what you think of the data model, and what you would build if your users' health data were one SQL query away. Issues, discussions and pull requests are all open.&lt;/p&gt;

</description>
      <category>api</category>
      <category>backend</category>
      <category>opensource</category>
      <category>softwaredevelopment</category>
    </item>
  </channel>
</rss>
