Dr Rob's Biometrics → Healthy Coins API Integration Spec v1.0 (21 August 2026) compared against what RKWO has actually built and what is running in production today.
The shipped build honours the privacy half of Rob's spec almost perfectly and skips the integrity half almost entirely. That is a fixable asymmetry, but it is the whole difference between a demo and something a health plan will underwrite.
The phone scores, the server banks. health_coin_awards genuinely has no column that
could hold a heart rate, a weight or a blood pressure. Rob's central claim — "there is no column
in the ledger that could hold them" — is true of our production schema today.
That is the sentence we get to say to a school district or a payer, and it survives inspection.
Rob's ledger stores rule_id and n — which rule fired and how many
units — so the server can recompute every award and discard whatever the client claimed.
Ours stores only a coin_group and a final coin total. We cannot recompute anything.
The number the phone sends is the number we bank, and — because of the RLS update policy — the
number the member can afterwards edit.
Everything else in this document follows from this one table difference.
player_id · local_date · rule_id · n · coins · event_id (UNIQUE) · envelope_id · created_at
rule_id — which of the rules paid.n — the count. Server recomputes from it.event_id UNIQUE — replays refused, retries free.envelope_id — ties the row to a signed payload.player_id — pseudonymous ULID, not an account id.user_id · coin_group · local_day · coins · source_metric (now empty) · updated_at PRIMARY KEY (user_id, coin_group, local_day)
n. Recomputation is impossible.user_id references auth.users directly.source_metric exists in the schema but is no longer written.Adding rule_id and n to the award row unlocks all four at once, and it costs
nothing in privacy terms: a step count is Green tier in Rob's own sensitivity gate
— "behaviours and counts … full scoring, full transparency." Rob's spec explicitly permits the count
to cross the wire. It is the Yellow and Red tiers — weights, blood pressures, clinical flags —
that must never leave the phone, and none of those are in scope for the ten shipped metrics.
Rob and RKWO both say "the phone scores." They mean different things by it.
| Layer | Rob's spec | Shipped |
|---|---|---|
| Who scores first | Phone, from the local rules bundle | Phone, from hardcoded rates match |
| Who scores last | Server recomputes from (rule_id, n). The client's coins field is discarded. | Nobody. The client's number is written verbatim. gap |
| Rules source | Versioned Airtable export, rules_version stamped on every envelope | Hardcoded in the app binary, unversioned gap |
| Rules update path | Airtable edit — no App Store release | Requires an App Store release gap |
| Payload signing | HMAC-SHA256, per-install key in Keychain / Keystore | None gap |
| Install attestation | App Attest (iOS) / Play Integrity (Android) | None gap |
| Idempotency | Client-generated ULID per event, unique index | Day-level only, via the primary key partial |
| Caps enforced | Server-side, against the ledger | Client-side only gap |
-- coin_events (FFH's own, 2026-05-22) — anyone can mint coins for anyone create policy "insert_own" on public.coin_events for insert with check (true); -- health_coin_awards — a member can rewrite their own coin totals create policy "health_coin_awards_update_own" on public.health_coin_awards for update using (auth.uid() = user_id) with check (auth.uid() = user_id);
Rob's design makes client tampering pointless — you can inflate n, but the server
recomputes and the caps bite. Our design makes it unnecessary: a member can PATCH their own
coin total straight through the REST API. The leaderboard is currently an honour system with a
login screen in front of it.
Drop the UPDATE policy on health_coin_awards and make the insert path a
security definer function that clamps to the per-group daily cap. That removes
self-service coin editing tonight and does not require the app to change at all. It is not
Rob's full design, but it turns "anyone can" into "nobody can" while we wait.
These are not the same rate card with rounding differences. They price the same behaviour up to twelve times apart, and only one of them was calibrated against what a Healthy Coin buys.
| The same behaviour | Rob's spec | Shipped | Shipped pays |
|---|---|---|---|
| 4,000 steps | 40 | 266 | 6.7× |
| 10,000 steps | 100 | 666 | 6.7× |
| 22,500 steps | 120 (cap) | 1,500 (cap) | 12.5× |
| 30 min vigorous exercise | 300 | 300 | 1.0× |
| 150 min workout | 300 (cap) | 1,500 | 5.0× |
| 20 mindful minutes | 150 (cap) | 200 | 1.3× |
| 8 hours sleep | 100 | 96 | 1.0× |
| 12 stand hours | 120 | 60 | 0.5× |
| Ceiling | Coins/day | Note |
|---|---|---|
| Rob's stated hard ceiling | 1,500 | "A good day lands near 500, a great day near 1,000" |
| Rob's own per-rule caps, summed | 1,530 | minor exceeds his own stated ceiling by 30 — worth a line in his v1.1 |
| Shipped — iOS | 2,510 | 67% above the design ceiling |
| Shipped — Android | 2,150 | Mindful minutes and stand hours have no Android source |
9,000 steps · 20-minute workout · 10 mindful minutes · 10 stand hours · 8 hours sleep → Rob: 640 coins. Shipped: 1,167 coins. Rob's open question #4 — "does 500 coins for a good day sit right against what Healthy Coins already buy in Reality Health Games?" — was never answered, and the app shipped at roughly double his figure. Every redemption price in Reality Health Games is now calibrated against the wrong denominator.
Mindful minutes (300/day) and stand hours (60/day) are iOS-only with no Android substitute in their group. An Android member's ceiling is 360 coins/day lower — 14.3%, or 10,800 coins over a 30-day challenge. Two equally active members on different phones do not compete on equal terms. For an organisation whose mission is health equity, a leaderboard that is unfair by construction is the finding I would raise before the rate card itself.
Rob's is calibrated to redemption value. RKWO's is calibrated to feel generous. Both are defensible; running both at once is not. The decision needs to be made once, written into a versioned rules bundle, and stamped on every award — otherwise coins earned in September and coins earned in November are not the same unit and the ledger cannot be audited.
"Any app can write steps into Apple Health. Without provenance filtering, 'gamified steps' is an honour system." His filter excludes hand-typed samples and our own writes:
// iOS let notManual = HKQuery.predicateForObjects( withMetadataKey: HKMetadataKeyWasUserEntered, operatorType: .notEqualTo, value: true) // Android val kept = records.filter { it.metadata.recordingMethod == RECORDING_METHOD_AUTOMATICALLY_RECORDED || it.metadata.recordingMethod == RECORDING_METHOD_ACTIVELY_RECORDED }
Status: unknown. We have never been sent RKWO's read code and cannot see it. This is a direct yes/no question for Rahul, and it is the one that decides whether the leaderboard means anything. It also has to be asked before Dr Rob's watch test, or the test proves the wrong thing.
| Rob's four layers | Status | Note |
|---|---|---|
| 1 · Provenance filter (client) | unknown | Ask Rahul. Cannot be verified from our side. |
| 2 · Server recomputes every award | not built | Impossible without n. |
| 3 · Caps everywhere, against the ledger | client only | Caps exist in the app; server does not enforce. |
| 4 · Attestation, signing, idempotency | not built | No HMAC, no App Attest, no event id. |
Worth stating plainly, because it is the part that is genuinely hard to retrofit and we did not have to.
| Rob's principle | Shipped |
|---|---|
| The phone is the scorekeeper, the server is the bank. Raw measurements never leave the handset. | honoured Conversion happens in the app; only a coin total is POSTed. |
| No column in the ledger could hold a health value. | honoured health_coin_awards holds a group, a date and an integer. Nothing else. |
| Collapsed disclosure by default — bucket codes rather than per-metric rule ids. | honoured — accidentally. coin_group is a 7-bucket collapse. But it is collapsed for Green rules too, where Rob allows full transparency, which is why we lost the demo. |
Per-day partitioning on local_date so no timezone reconstruction is possible. | honoured local_day is part of the primary key. |
| Read-only scopes, no write access requested. | believed Consistent with what we see; unverified. |
| Streak scored from dates alone, never from health data. | honoured The streak engine reads only coin totals and dates. |
Rob's rate card pays a streak 250 / 750 / 2,500 coins at 7 / 30 / 100 days. Our Streak Challenge page runs 3 / 7 / 14 / 21 / 30 days and pays nothing at any milestone. Neither the ladder nor the payout matches the spec. That is our gap, not RKWO's, and it is a small fix — but the two ladders should be reconciled before the page is shown to a partner.
"No HIPAA obligations arise in Phase 1 because there is no protected health information to
protect — FFH never receives any." Applied to what is actually in production: a table of
(user_id, coin_group, local_day, coins) contains no health information, no inference
about an identified person's health status, and nothing a Business Associate Agreement would attach
to. The unsigned Supabase BAA is not a blocker for Phase 1 as built. It is an absolute
blocker for Phase 3.
health_coin_awards. The lit_* tracker tables and
any free-text tracker entries are a separate question and have not been reviewed against this
standard. One table being clean does not make the database clean.The story that holds up is: "Phase 1 handles no PHI by construction — here is the schema, there is no column that could hold a measurement. Phase 3, where a member opts in to share detail with their clinician, is a separate service with a separate database and a separate consent, and it is not built yet." Rob's Wall — build Phase 3 as a new service, never as a migration — is the part to commit to out loud, because it is the promise that makes Phase 1 credible.
Dr Rob has the watch. Here is precisely what a test can and cannot demonstrate with today's schema, and what one added column would change.
| Link in the chain | Provable today? | How |
|---|---|---|
| Watch → iPhone HealthKit | yes | Apple Health app shows the sample with the watch as source. |
| HealthKit → FFH app read | yes | The app's own dashboard renders the reading. |
| App conversion → coins | no | The gap. Without n we see the output but never the input. We cannot show the arithmetic. |
| Coins → Supabase row | yes | Row appears for that user_id, local_day, coin_group. |
| Which metric earned it | no | source_metric is in the schema but is no longer being written. |
| Sync freshness | yes | updated_at gives real latency: now − max(updated_at). |
| Supabase → web page | yes | The Streak Challenge page already renders it live. |
That is a manual reconciliation, not a system proof. It demonstrates connectivity convincingly and it
is enough for a partner demo — but it does not scale, it cannot be automated, and it cannot be
repeated by anyone who is not holding Rob's phone. Restoring source_metric and adding
n turns a five-step manual ritual into a line on a page. That is the argument to
make to Rahul, and it is a better argument than "the Vault needs it" because he can see the value
this week.
Ordered by value per unit of his effort, not by how much we want them.
| # | Ask | Effort | Why it earns its place |
|---|---|---|---|
| 1 | Add rule_id and n to the award row; restore source_metric | small | Unlocks server recomputation, the FHIR Observation code, the Provenance resource, and the Apple Watch demo — all four, from one change. Green-tier data by Rob's own gate. |
| 2 | Confirm the provenance filter is implemented | answer | Yes or no. If no, the leaderboard is an honour system and we should know before we promote it. |
| 3 | Take MAX within a coin group, not highest-priority | small | Fixes the Q3 bug his own worked example #7 demonstrates — a member currently loses up to 1,200 coins for logging a run. |
| 4 | Stamp rules_version on every award | small | Without it, a future rate change silently restates history and the ledger stops being auditable. |
| 5 | Close the Android parity gap, or exclude iOS-only groups from challenge scoring | medium | 14.3% ceiling difference. A mixed leaderboard is currently unfair by construction. |
| 6 | Move the rules bundle out of the binary | medium | Rob's design. Rate changes without an App Store release. Not urgent, but every week it waits the migration gets bigger. |
| 7 | HMAC signing + App Attest / Play Integrity | large | The real fix for coin integrity. Phase 2 work — but the schema decisions in #1 and #4 should not foreclose it. |
health_coin_awards; clamp inserts in a security definer function.coin_events insert policy — with check (true) has been live since 22 May.