Writing a receiver
A receiver is anything that accepts POST of the schema in
schema.md and answers with a status. Health Relay assumes
nothing else about it.
The minimum
Section titled “The minimum”- Accept
POSTwith a JSON body up to 16 MB. - Check the bearer token if you issued one; answer 401 when it is wrong. The app stops sending until the user fixes it.
- Answer 400 with
{ok: false, error, message}whenschemais notapple-push/1or a section is not an array. The app showsmessage. - Answer 2xx with
{ok: true, ...}to an empty push (days: []). That is the app’s connection test. - Store what you need and answer 2xx. Answer 5xx only when the app should retry the same batch later.
tools/receiver.mjs is all of that in about a
hundred lines of Node with no dependencies. Run it with
node tools/receiver.mjs --token secret --out /tmp/pushes and point a
destination at http://<this machine's LAN IP>:8787. It prints each push’s
days and per-type counts, and --out saves every body for inspection.
--fail 503 makes it refuse everything, to watch an app’s retries.
Storing days correctly
Section titled “Storing days correctly”The rules that make a receiver agree with the phone:
- A listed day is owned. For each day in
days, replace everything you hold for that day from this phone with what the push carries. Something the phone no longer has (a deleted workout) must disappear. If you keep only some metrics, readcoversand own only what it lists. - Records outside
daysonly add. A night that began the evening before a listed day arrives with it; add it, do not replace that earlier day. - Newer reads win. Batches can be retried, so an older read can arrive
after a newer one. Keep
exportedAtper day and ignore a push older than what you hold; that is still a 2xx. - Combine sources as schema.md says: the largest source total for
sums,
count-weighted pooling for means and heart rate, the latestlatestAtfor spot values, energy-weighted averages for sound levels. - Days are local.
day,daysand the date part of every timestamp are the phone’s local date; do not convert them to UTC days.
Worked example: cubby
Section titled “Worked example: cubby”cubby’s health app (a personal dashboard) implements this schema at
POST https://<host>/_cubby/apps/health/push:
- Auth. The user mints a device token on the health app’s Data tab (“Add a phone”); it is shown once and only its SHA-256 is stored. In Health Relay the user adds an HTTP endpoint destination with that URL and token.
- Parsing. The push runs through the same aggregator as an Apple
Health
export.zipupload, so a pushed day and an exported day combine identically. Eachstatsrow is folded in as its mode says;samplesgo through the export.xml record path. - Ownership. Listed days replace every Apple metric and event kind
the dashboard keeps, narrowed by
coverswhen present. - Precedence. Pushes and manual exports share one ledger keyed by
exportedAt, so whichever was read from Health later wins per day; a stale push answers 2xx withdays_stalecounted. - Answer.
{ok: true, months_written, months_unchanged, days_stale, days, from, to};400 bad_pushorbad_schemafor a body that is not the schema;401 token_unknown;500 write_failedwhen its store failed.
fixtures/push-two-days.json produces, in cubby, the same days, events
and ownership as cubby’s own test body.