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.
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:
| Thing | Cost |
|---|---|
| Hosting and the function that receives your data | Free. The usage here is nowhere near the limits of a free plan. |
| The iPhone app that does the sending | A 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 one | Roughly the price of a coffee per year. Entirely optional; you get a working web address for free either way. |
| Your time | An evening. |
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.
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.
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.
my-health. Set it to Private. Do not tick
the box that adds a README.public folder, the
netlify folder, the scripts folder, and the loose files sitting
alongside them.
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.
.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.
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:
INGEST_TOKEN, spelled exactly like that. The value is
your random string.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.
Install Health Auto Export and give it permission to read your health data when it asks. Then build one automation:
/api/ingest on the end. For example
https://your-site.netlify.app/api/ingest.Authorization and the value is the word
Bearer, then a space, then your secret. So:
Bearer your-long-random-string.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.
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:
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.
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.
| File | What it holds |
|---|---|
public/labs.json | Your 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.json | Anything you have from a genotyping service, grouped into categories, with a confidence flag on each entry. |
public/supplements.json | What you take and why, with an honest grade on how good the evidence actually is. |
public/index.html | The 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.
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.
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:
| What you see | What 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. |
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.
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.
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.
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.
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.
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.