Back to the dashboard

Build your own

Everything on this site runs with no server, no database and no monthly bill. Here is how to put your own version together. It took me an evening the first time, and most of that evening was spent on one problem I will save you.

Joel Waldman

What is in here

  1. What you end up with
  2. The one constraint that shapes everything
  3. What you need before you start
  4. Getting the site online
  5. The one password in the system
  6. Setting up the phone
  7. Checking it actually worked
  8. Loading your back catalogue
  9. Making it yours
  10. Your own domain
  11. Things that will go wrong
  12. Four traps worth knowing about
  13. What it cannot do

1. What you end up with

A public web page that shows your own health data and keeps itself up to date. Your watch data arrives on its own every hour. Bloodwork and DNA you type in yourself, a few times a year at most. Nobody has to remember to do anything.

The shape of it is small enough to hold in your head:

iPhone  ->  every hour  ->  a small function on the internet  ->  a key/value store
                                                                        |
                                                                        v
                                    your web page  <-  reads that store on load

That is the whole system. There is no database to administer, no server that can fall over at 3am, and nothing to patch. The page itself is one HTML file. If every service I use disappeared tomorrow, you could open that file on your laptop and it would still work.

What it costs:

ThingCost
Hosting and the function that receives your dataFree. The usage here is nowhere near the limits of a free plan.
The iPhone app that does the sendingA one-time purchase, and it was a few dollars when I bought it. Check the App Store for the current price.
A domain name, if you want oneRoughly the price of a coffee per year. Entirely optional; you get a working web address for free either way.
Your timeAn evening.

2. The one constraint that shapes everything

Before anything else, you need to understand why this is built the way it is, because otherwise half the design looks like a strange choice.

There is no way to ask Apple for your health data. Apple Health lives on your phone. It is not in the cloud in any form you can reach, and there is no address anywhere on the internet that will hand it to you, no matter who you are or what you are willing to pay. Your own data, your own phone, and nothing can go and get it.

I spent a genuinely embarrassing amount of time looking for the API that does not exist. Every service that shows you your Apple Health data works the way this one does: something on the phone pushes the data out. That is the only door there is.

Once you accept that, the rest follows. The phone has to send on a schedule. Whatever receives it has to be happy to be handed the same day twice, because the phone will do exactly that. And the page has to be honest about the fact that a missing day might mean you did nothing, or might mean your watch was on a charger. Those are different things, and a lot of health apps quietly treat them as the same.

3. What you need before you start

What is in the download. The same page you are reading this on, the code that receives your data, the whole test suite, and a copy of this guide. The three data files come with worked examples rather than my results: two invented blood panels, two example genome entries and two example supplements, all clearly marked, so you can see the shape of each before you replace it.

There are no images in it, because every icon on my site is a photograph of me. Add your own favicon.png, apple-touch-icon.png and og.png to public/ when you want them, and put the matching tags back in the head of index.html.

You do not need to know how to code. You do not need to install anything on your computer, and you never have to open a terminal. Everything below happens in a web browser and on your phone. If you do write code, there is a command line route as well, and it is faster, but it is not the one I am going to walk through.

4. Getting the site online

The goal here is to get the project files onto GitHub, and then point Netlify at them. Netlify does the rest by itself, including installing anything the project needs.

Put the files on GitHub

  1. Sign in to GitHub and click New repository.
  2. Name it something like my-health. Set it to Private. Do not tick the box that adds a README.
  3. On the empty repository page, click the link that says uploading an existing file.
  4. Unzip the project. Open the unzipped folder and drag the things inside it into the browser window. Not the folder itself, the contents: the public folder, the netlify folder, the scripts folder, and the loose files sitting alongside them.
  5. Type "first commit" in the message box and click Commit changes.

If you ever see a folder called node_modules, do not drag it. It contains thousands of files, the upload will crawl, and none of it is needed. It is not in the download, but it appears the moment you run the project on your own machine, so it is worth recognising.

Point Netlify at it

  1. Sign in to Netlify with your GitHub account.
  2. Click Add new project, then Import an existing project, then GitHub. Authorise it and pick the repository you just made.
  3. It will show you some build settings. Accept them. The project carries a configuration file that tells Netlify everything it needs, so there is nothing to fill in.
  4. Wait about a minute. You will get a web address ending in .netlify.app. Open it.

You should see the site, populated with realistic looking fake data. That is deliberate: the page ships with sample data so you can see whether the layout works before any of your own numbers exist. Real data replaces it automatically the moment your phone sends anything.

Do not use Netlify's drag and drop deploy. There is a tempting box on their site that lets you drop a folder straight in, no GitHub involved. It will look like it worked. It will even show you the page. But that route does not carry the small piece of code that receives your data, so your phone will have nowhere to send anything and the site will show sample data forever. Their own support documentation is blunt about this. Use the GitHub route.

5. The one password in the system

The address your phone posts to is open to the internet, so it needs to know that a request really came from you. That is done with a shared secret: a long random string your phone sends with every upload, which the receiving code checks before it writes anything.

Make one. Any long random string will do, and there are plenty of generators online. Aim for something like 40 characters of letters and numbers. It should look like line noise, not like a password you could remember.

Then, in Netlify:

  1. Open your project, then Project configuration, then Environment variables.
  2. Add a variable. The name is INGEST_TOKEN, spelled exactly like that. The value is your random string.
  3. Go to Deploys and click Trigger deploy. The code only picks up the secret when it next deploys, so skipping this step is a classic way to lose twenty minutes.

Keep the secret in Netlify only. It belongs in that settings screen and in your phone, and nowhere else. Do not put it in the project files, because those go to GitHub, and anything that reaches GitHub is very hard to properly remove afterwards. If you ever decide to make your repository public, check its history first.

6. Setting up the phone

Install Health Auto Export and give it permission to read your health data when it asks. Then build one automation:

  1. Go to Automations and create a new one.
  2. Set the type to REST API.
  3. The URL is your site address with /api/ingest on the end. For example https://your-site.netlify.app/api/ingest.
  4. Method: POST. Format: JSON.
  5. Add a header. The name is Authorization and the value is the word Bearer, then a space, then your secret. So: Bearer your-long-random-string.
  6. Set it to run every hour, and set the period to the last day or similar. Sending the same day repeatedly is fine and expected.
  7. Choose which metrics to send. Tick everything that looks useful. At minimum: steps, resting heart rate, heart rate variability, exercise time, active energy, basal energy, VO2 max, weight, respiratory rate, and sleep analysis. Also enable workouts if the app offers them separately.

Tick basal energy while you are in there. If you skip it, the page has to estimate your resting burn from your body composition rather than reading what your watch actually measured. The estimate is decent, but measured beats estimated.

Run the automation once by hand. The app should report a success. If it reports a 401, the secret does not match; check for a stray space, and check that you triggered a deploy after saving it.

7. Checking it actually worked

Open your site with /api/data on the end, so https://your-site.netlify.app/api/data. You should see a wall of raw data. Somewhere near the top there is a count of how many days are stored, and a timestamp of the last update.

Three outcomes:

8. Loading your back catalogue

So far you have today. If you have been wearing a watch for years, all of that history is sitting on your phone and can be sent in one go.

In Health Auto Export, look for an export or history option and give it a wide date range, then point it at the same address with the same header. It will take a while. Mine sent about four years of daily data in a few minutes.

Nothing bad happens if this overlaps with what is already stored. The receiving code merges by day: a day that arrives twice is updated, not duplicated, and existing values are never deleted by a later upload that happens not to include them. That property matters more than it sounds, and it is worth checking in any version of this you build. The failure it prevents is a partial upload quietly wiping a day that was already complete.

9. Making it yours

Everything you would want to change lives in a handful of files, and you edit them on the GitHub website. Click into the file, click the pencil icon, make your change, click Commit changes. The site rebuilds itself in about twenty seconds. There is nothing else to it, and it is the whole reason this is set up the way it is: four times a year I paste in a lab result and I am done.

FileWhat it holds
public/labs.jsonYour bloodwork. Each panel is a date and a set of results. There is a separate list describing what each marker means, which is what fills in the explanations.
public/dna.jsonAnything you have from a genotyping service, grouped into categories, with a confidence flag on each entry.
public/supplements.jsonWhat you take and why, with an honest grade on how good the evidence actually is.
public/index.htmlThe page itself. Near the top there is a short settings block holding your age bracket, your sex, and the thresholds the training section uses.

These files are picky about punctuation, which is the price of them being simple. Every entry needs a comma after it except the last one in a list. If the page goes blank after an edit, that is almost always the reason. Paste the file into any online JSON checker and it will point at the line.

A word on the bloodwork file

Transcribing lab results by hand is dull and it is the step where errors creep in. Two habits that saved me: type the units in as well as the number, and record the reference range your lab printed rather than a range you found online. Reference ranges differ between labs, and comparing your value against the wrong one is a good way to frighten yourself for no reason.

10. Your own domain

Optional, and it makes the thing feel real. Buy a domain wherever you like. Then in Netlify, open Domain management and add it as a custom domain. Netlify tells you exactly which records to create at your registrar.

You do not have to move your domain to Netlify to do this. I kept mine where I bought it and just pointed the records. Two things worth knowing:

11. Things that will go wrong

What you seeWhat it usually is
The page shows fake data forever Nothing has arrived. Check /api/data to see whether the problem is the phone or the site.
/api/ingest gives a 404 The function did not deploy. Nearly always the drag and drop route. Connect the repository properly.
/api/ingest gives a 401 The secret does not match. Check for a trailing space, check the word Bearer and the space after it, and make sure you redeployed after saving the variable.
The site went blank after you edited a file A missing or extra comma in the file you just edited. Paste it into a JSON checker.
Sleep data is patchy Not a bug. If you charge your watch overnight, it is not measuring your sleep. Worth knowing before you read anything into a sleep chart.
Every date is one day off A time zone problem, and the most annoying bug I hit. See below.

12. Four traps worth knowing about

These are the ones that cost me real time. If you build your own version rather than using mine, every one of them is waiting for you.

Missing is not zero

My first version treated a day with no exercise reading as a day with zero exercise. That is a reasonable looking line of code and it is completely wrong. It broke my workout streak on a day I had trained, because the watch had not finished syncing, and it left holes in the chart that looked like rest days.

A day has three possible states, not two: you trained, you rested, or nobody knows. A day with no record at all is the third one. It should be stepped over, not counted as a rest day. Absence of evidence is not evidence of absence, and on a health page that distinction is the difference between a chart that tells the truth and one that flatters you.

Dates are harder than they look

A date with no time attached gets treated as midnight, and midnight in one time zone is the previous evening in another. My whole page was showing every date one day early, and I only noticed because a number I recognised was attached to the wrong day. It had been wrong from the first version.

If you are building this yourself: pick one interpretation, apply it everywhere, and write a test that runs in several time zones. It is a boring bug that hides in plain sight for weeks.

A run of days is not a run of independent facts

This one is subtler and it changed what my site is willing to claim. If you correlate two health metrics across a year, you have 365 readings, but you do not have 365 independent pieces of evidence. My weight tomorrow is mostly my weight today. My VO2 max barely moves week to week. Treating each day as fresh evidence makes weak relationships look convincing.

Worse, almost everything drifts over a long enough period. Two numbers that both climbed over two years will correlate strongly whether or not they have anything to do with each other. My training volume and my fitness both went up, and the correlation between them looked like the strongest thing on the page until I checked whether it survived taking the trend out. It did not. That does not prove there is no effect, but it does mean I cannot tell the difference, and saying so is the only honest option.

Your own body will produce a false positive eventually

Screen enough markers and something will come back out of range. Most of the time it means nothing. I take creatine, which raises one of the standard kidney markers directly, without the kidneys doing anything different at all. If you did not know that, you would look at my results and worry.

Write down what you take and what it does to your bloodwork, next to the bloodwork. Future you will not remember, and neither will the doctor reading it cold.

13. What it cannot do

Being clear about this is most of the value, so it goes last where it will be read rather than buried in a footnote.

With all of that said, it has been worth building. Not because it told me anything dramatic, but because having the numbers in one place, with the uncertainty shown rather than hidden, changed what I bother paying attention to. Most of what I thought was signal turned out not to be. That is a useful thing to learn about yourself.