Store Health Connect webhook payloads in Supabase

Receive HC Sync webhooks on your backend, verify the timestamp and signature against the original request bytes, then insert the verified payload into a private Supabase Postgres table. An Android app still supplies the Health Connect data; Supabase stores the downstream events.

Last updated: 2026-11-04. API source: HC Sync documentation.

What table should receive verified payloads?

create table public.hcsync_events (
  event_id text primary key,
  payload jsonb not null,
  received_at timestamptz not null default now()
);
alter table public.hcsync_events enable row level security;
-- No client policies: write through your trusted receiver only.

This is an application-owned event inbox, not an HC Sync API schema. Run the SQL in your Supabase database. Use a trusted server database connection in DATABASE_URL and keep it out of clients. Set HCSYNC_WEBHOOK_SECRET separately.

How do you verify before inserting?

Install express and pg in your receiver project. The signature checks below follow the documented Node verifier: numeric timestamp, five-minute freshness, lowercase 64-character hex signature, HMAC-SHA256 over timestamp + dot + raw bytes, and timing-safe comparison. Mount this route before JSON middleware.

import express from "express";
import { createHmac, timingSafeEqual } from "node:crypto";
import pg from "pg";
const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });
const app = express();
const secret = process.env.HCSYNC_WEBHOOK_SECRET;
if (!secret) throw new Error("Set HCSYNC_WEBHOOK_SECRET");
function verify(ts, sig, raw) {
  if (!/^\d+$/.test(ts) || Math.abs(Date.now()/1000 - Number(ts)) > 300 || !/^[a-f0-9]{64}$/.test(sig)) return false;
  const want = createHmac("sha256", secret).update(ts + ".").update(raw).digest();
  return timingSafeEqual(Buffer.from(sig, "hex"), want);
}
// Register before express.json(): signatures cover the original bytes.
app.post("/hcsync", express.raw({ type: "application/json", limit: "1mb" }), async (req, res) => {
  if (!Buffer.isBuffer(req.body) || !verify(req.get("x-hcsync-timestamp") || "", req.get("x-hcsync-signature") || "", req.body)) return res.sendStatus(401);
  let event;
  try { event = JSON.parse(req.body.toString("utf8")); }
  catch { return res.sendStatus(400); }
  if (event?.event === "test" && !event.id) return res.sendStatus(204);
  if (typeof event?.id !== "string" || !event.id) return res.sendStatus(400);
  try {
    await pool.query(
      "insert into public.hcsync_events(event_id,payload) values ($1,$2::jsonb) on conflict (event_id) do nothing",
      [event.id, JSON.stringify(event)]);
  } catch {
    return res.sendStatus(503);
  }
  return res.sendStatus(204);
});
app.listen(3000);

The receiver acknowledges signed test events without inserting them; real events require an ID. A primary key deduplicates retries. A worker must process this inbox and apply record and user deletions to any derived tables; retaining an inbox does not implement deletion handling.

How do you connect and operate the receiver?

curl -X PUT https://hcsync.dev/v1/webhook \
  -H "authorization: Bearer $HCSYNC_SECRET" \
  -H 'content-type: application/json' \
  -d '{"url":"https://your-server.example/hcsync"}'
curl -X POST https://hcsync.dev/v1/webhook/test \
  -H "authorization: Bearer $HCSYNC_SECRET"

Return a 2xx within 5 seconds, after durable acceptance. Deduplicate event IDs; test events have no ID. Handle records.synced, records.deleted and user.deleted. Queued events retry in order every 5 minutes, on sync or replay, for up to 7 days, signed afresh. The queue holds 500 events per project; new events are discarded when full. Record writes and event queuing are separate, so a failure between them can lose an event. REST exposes current records but cannot recover missed deletion events.

Restrict database access and define retention and deletion for stored health payloads. The webhook guide covers delivery inspection and replay.

Frequently asked questions

Can Supabase read Health Connect directly?

No. An Android app must read permitted Health Connect data and upload it to HC Sync first.

Should the receiver verify parsed JSON?

No. Verify the original request bytes before parsing or storing the payload.

Python guide · Expo guide · API documentation

Create a free project