Confidential · Internal & named recipient only · Do not forward
Engineering & Compliance Handoff · Confidential

LIVE It Trackers → Flutter Mobile App

Everything you need to scope, price and build the native mobile layer for The Force for Health® Network — the ten LIVE It Trackers, their PHI-bearing data model, the compliance rules that are not optional, and the exact contract between native code and our web platform.

Version 1.0 · 20 August 2026 Prepared by the FFH Compliance Team Audience: incoming mobile developer Est. read: 25 minutes
10LIVE It Trackers
13PHI tables (lit_*)
59RLS policies
4Compliance patches shipped
5Go-live gates open
1

Start Here

The shape of the job, in one page.

What we are asking you to build

The Force for Health® Network runs a large web platform at theforce.health. Inside it sits the LIVE It Trackers — a single-page health-tracking application where members log pain, weight, nutrition, medication and treatment response, screen time, workouts, service hours and personal challenges. That data is Protected Health Information and is already wired to Supabase behind row-level security.

We are commissioning a Flutter hybrid shell that (a) hosts the existing web platform, (b) adds genuinely native capability the web cannot provide, and (c) becomes the long-term container for the rest of our content. The native capability that matters most is two-way health data sync: Apple HealthKit on iOS, Android Health Connect on Android, plus Bluetooth medical devices.

Your deliverable is not "wrap the website." A wrapper alone will be rejected by Apple. Your deliverable is a native health-data layer with a web presentation surface bolted to it.

Read this before you quote This app handles PHI under HIPAA, and a meaningful share of our members are minors — which brings COPPA and, in school settings, FERPA. Compliance is not a phase at the end. It constrains the architecture from line one. Section 4 lists the rules that are not negotiable; if any of them are a surprise to you, tell us now rather than at submission.
The two things that will kill your timeline Apple Guideline 4.2 ("minimum functionality") rejects repackaged websites — your defence is a visible native health feature, and it must be visible in the screenshots. Google Play organization verification (D-U-N-S) is required for health apps and is the long pole: start it on day one, not at submission.

How to read the rest of this document

SectionWhat it gives youWhy you need it
2 · What the Trackers AreThe ten trackers and what each one capturesScope. Every one of these is a sync candidate.
3 · Data ArchitectureFull data dictionary for 13 lit_* tables + the RLS modelYou will read and write these. Field names are exact.
4 · ComplianceSeven hard rules, four shipped patches, five open gatesBreak one of these and we cannot ship at all.
5 · Bridge ContractThe window.FFHNative JS ⇄ Dart API you must implementThis is the actual interface spec. Build to it.
6 · Field MapEvery tracker field mapped to HealthKit + Health Connect typesRemoves the guesswork and the 100× unit bugs.
7 · DevicesOmron, AliveCor Kardia, generic BLE profilesPhase 2. Some need signed partner agreements.
8 · Built vs. To BuildHonest inventory of what already existsSo you do not re-quote work that is done.
9 · IP & SecurityAssignment, disclosure, and no-PHI-on-your-machine rulesThese will appear in your contract. No surprises.
10 · Open DecisionsTwelve questions we have not answered yetYour first deliverable is your recommendation on these.
2

What the LIVE It Trackers Are

Ten trackers in one single-page app. All of it is PHI.

"LEARN It · LIVE It · SHARE It" is our member framework. The LIVE It Trackers are the LIVE It half — the place a member records what is actually happening in their body and their week. They currently live at /live-it-trackers/ as one large self-contained page (~1.2 MB, ~13,800 lines) that talks directly to Supabase from the browser.

Every tracker awards LIVE It Coins and feeds streaks, badges and group leaderboards. That gamification layer is the reason members come back, and it is also the reason the leaderboard privacy rules in Section 4 exist.

#TrackerWhat the member logsPrimary tableSensitivity
1Wellness / Daily Check-inMood, energy 1–5, sleep hours, water, steps, exercise minuteslit_wellness_logPHI
2Weight & BMIWeight with lbs/kg toggle, goal weight + target date, BMI calculatorlit_weight_entries
lit_weight_goals
PHI
3PainBody location, 0–10 severity, pain type, timestamp, noteslit_pain_entriesPHI
4Treatment ResponseTreatment name + category, pain before/after, effectiveness 1–5, durationlit_treatment_entriesPHI
5NutritionFood diary by meal, macros, micros, barcode scan, calorie/macro goalslit_nutrition_diary
lit_nutrition_goals
PHI
6Screen TimePer-app minutes (JSONB), daily goal hours, trend + donut chartslit_screen_timeBehavioural
7WorkoutsSaved routines with exercise library, sets/reps/rest; session history with XPlit_workouts
lit_workout_history
PHI
8Reactions & ProductsPersonal-care/food products, OCR'd ingredient lists, symptoms, severity, safety enginelit_reactions_productsPHI
9SERVE ItVolunteer/service hours, dollars, units, beneficiary, impact scorelit_serve_it_entriesPII only
10ChallengesPersonal + monthly community challenges, streaks, milestones, calendarlit_challengesBehavioural
Also in the page today Blood pressure, heart rate (rest/avg/peak) and sleep surfaces exist in the UI and charting layer, sourced from demo data. They have no dedicated Supabase table yet — a separate biometric_logs table serves the Biometrics module. Unifying these is Open Decision #3.
Non-obvious dependency The page pulls three heavy third-party libraries at runtime: Chart.js (all visualisations), Tesseract.js (ingredient-label OCR) and html5-qrcode (barcode scanning). Barcode and OCR both want the camera. Inside a WebView those need native permission plumbing, and the native camera is a better experience than the WebView one — factor this into your estimate.
3

Data Architecture

Supabase Postgres. 13 tracker tables, 3 compliance tables, 59 RLS policies.

The stack in one diagram

     ┌──────────────────────────────────────────────────────────┐
     │  FLUTTER SHELL  (you build this)                         │
     │  ┌────────────────────┐   ┌───────────────────────────┐  │
     │  │ Native health layer│   │  WebView → theforce.health│  │
     │  │  · HealthKit       │◄─►│  · LIVE It Trackers page  │  │
     │  │  · Health Connect  │   │  · Academy, community,    │  │
     │  │  · BLE devices     │   │    checkout, everything   │  │
     │  │  · Push (FCM/APNs) │   └───────────┬───────────────┘  │
     │  └─────────┬──────────┘               │                  │
     │            └──── window.FFHNative ────┘   (§5)           │
     └────────────────────────┬─────────────────────────────────┘
                              │  supabase-js  (anon key + user JWT)
                              ▼
     ┌──────────────────────────────────────────────────────────┐
     │  SUPABASE  ·  project ref kfdwkiarqxlstlsnrilj           │
     │  auth.users → profiles → organizations                   │
     │  13 × lit_* tables    · RLS: user_id = auth.uid()        │
     │  consent_records · audit_log · data_requests             │
     └──────────────────────────────────────────────────────────┘
Architectural constraint — read twice Today the browser talks to Supabase directly with the anon key plus the signed-in user's JWT. Row-level security is the only thing standing between member A and member B's health records. That is a legitimate Supabase pattern, but it means every RLS policy is a security control, not a convenience. Do not add a service-role key to the mobile client. Do not proxy writes around RLS. If you believe a feature requires elevated privilege, it goes through a server-side route and you raise it with us first.

Data dictionary — the lit_* tables

lit_wellness_log PHI SYNC

One row per member per day — the daily check-in. UNIQUE (user_id, date), so this is an upsert target.

ColumnTypeNotes
iduuid pkgen_random_uuid()
user_iduuid→ profiles(id) ON DELETE CASCADE
organization_iduuid→ organizations(id). Drives leaderboard scope.
datedateNOT NULL
moodtextCHECK IN great · good · ok · rough · bad
energyintCHECK 1–5
sleep_hoursnumeric(3,1)Sync candidate ← HealthKit / Health Connect sleep
water_glassesintSync candidate ↔ HydrationRecord / dietaryWater
stepsintSync candidate ← stepCount / StepsRecord
exercise_minutesintSync candidate ← appleExerciseTime / derive from sessions
notestextFree text — treat as PHI
coins_earnedintGamification. Default 0.
created_at / updated_attimestamptzDefault now()
lit_weight_entries · lit_weight_goals PHI SYNC

Append-only weight log plus a goal record. The highest-value bidirectional sync target — members already weigh in on connected scales.

Table · ColumnTypeNotes
weight_entries.datedateNOT NULL. Indexed (user_id, date).
weight_entries.valuenumeric(5,1)NOT NULL
weight_entries.unittextCHECK IN lbs · kg — default lbs. Store the unit; never assume.
weight_entries.notestext
weight_goals.target_valuenumeric(5,1)NOT NULL
weight_goals.target_unittextCHECK IN lbs · kg
weight_goals.target_datedateNullable
Watch There is no uniqueness constraint on (user_id, date) for weight entries. A naive "sync everything since last run" will duplicate rows. Deduplicate on (user_id, date, value, unit) and carry a source marker — see Open Decision #5.
lit_pain_entries PHI

The most clinically sensitive table in the system. Many of our sickle-cell members use this to build a pain record they show a provider.

ColumnTypeNotes
datetimetimestamptzNOT NULL, indexed. Note: timestamp not date.
locationtextNOT NULL — body region
levelintCHECK 0–10
pain_typetextFree text
notes · coins_earnedtext · int
No health-platform equivalent Neither HealthKit nor Health Connect has a pain-severity type. This data stays FFH-only. Do not improvise a mapping into a symptom or mindfulness type.
lit_treatment_entries PHI

Medication and treatment response — what was taken, and did it work.

ColumnTypeNotes
datedateNOT NULL
nametextNOT NULL — treatment/medication name
categorytext
pain_before · pain_afterintPaired with the pain scale
effectivenessintCHECK 1–5
duration · duration_unitnumeric · text
notes · coins_earnedtext · int
Highest-sensitivity field in the schema name holds medication names. A medication list is among the most re-identifying and most damaging fields we hold. It must never appear in a log line, a crash report, an analytics event, a push notification body, or a leaderboard.
lit_nutrition_diary · lit_nutrition_goals PHI SYNC

One row per food item, not per meal — this matters for the Health Connect mapping, which is per-meal.

ColumnTypeNotes
datedateNOT NULL
mealtextCHECK IN breakfast · lunch · dinner · snack
food_name · brandtextfood_name NOT NULL
caloriesnumeric(6,1)kcal
carbs · protein · fat · fiber · sodium · sugarnumeric(6,1)grams — including sodium, which HealthKit expects in mg. Convert.
quantity · unitnumeric(5,2) · textDefault quantity 1
goals.calories/carbs/protein/fat/fiberintDefaults 1520 / 190 / 95 / 50 / 25
Structural mismatch HealthKit writes one sample per nutrient. Health Connect writes one NutritionRecord per meal carrying all nutrients. Our schema is per-food-item. You need a three-way translation, and you need to decide whether a meal is the sync unit or the item is. See Open Decision #6.
lit_screen_time BEHAVIOURAL
ColumnTypeNotes
datedateUNIQUE (user_id, date) — upsert target
app_datajsonbDefault []. Array of per-app minute records.
goal_hoursnumeric(3,1)Default 4
notestext
Do not automate this iOS Screen Time data is only reachable through Apple's Family Controls / DeviceActivity framework, which requires a separate Apple entitlement request and is designed for parental-control apps. Android's UsageStatsManager requires a special-access permission that Play scrutinises heavily. Both are out of scope. This tracker stays manual entry unless we jointly decide otherwise, and that decision goes through us first.
lit_workouts · lit_workout_history PHI SYNC

lit_workouts is the member's saved routine library; lit_workout_history is the session log. Only the history syncs.

ColumnTypeNotes
workouts.name · workout_typetextname NOT NULL
workouts.exercisesjsonbDefault [] — sets, reps, rest per exercise
workouts.total_sets · est_minutesint
history.workout_iduuid→ lit_workouts(id) ON DELETE SET NULL
history.datedateNOT NULL
history.exercises_count · duration_minutesint
history.xp_earned · coins_earnedintGamification
Gap History stores a date, not a start/end timestamp. HealthKit HKWorkout and Health Connect ExerciseSessionRecord both require a start and end instant. Either we add columns or you synthesise a window — a schema change is cleaner. Open Decision #7.
lit_reactions_products PHI

Powers the "My Beauty & Health" safety engine — members register baseline products and log reactions; the page cross-references ingredient lists.

ColumnTypeNotes
product_typetextCHECK IN baseline · reaction
name · brand · categorytextname NOT NULL
ingredientsjsonbDefault [] — populated by OCR or barcode lookup
ratingintStar rating
reaction · symptoms · severitytextClinical symptom data.
date · image_urldate · textImage goes to Supabase Storage, not the row
Camera + storage warning Ingredient photos and product shots are member-generated images that can contain faces, homes and prescription labels. They belong in a private Supabase Storage bucket, path-scoped to {user_id}/… with matching storage RLS — the pattern is already established for biometric uploads. Never write them to the device photo library, never to a public bucket, never to a CDN with a guessable URL.
lit_serve_it_entries PII ONLY
ColumnTypeNotes
date · service_typedate · textBoth NOT NULL
label · category · whotextwho = beneficiary — may name a third party
minutes · unitsintDefault 0
dollarsnumeric(10,2)Default 0
score · notes · coins_earnedtext · text · int
lit_challenges BEHAVIOURAL
ColumnTypeNotes
challenge_type · titletexttitle NOT NULL
start_date · end_datedatestart_date NOT NULL
target_value · current_valueintcurrent_value default 0
statustextCHECK IN active · completed · abandoned. Default active.
coins_earnedint
Sync opportunity Step- and workout-based challenges could auto-advance current_value from HealthKit / Health Connect instead of manual logging. That is the single most compelling native feature we can put in an App Store screenshot — and it doubles as our Guideline 4.2 defence.

The compliance tables — you will write to these too

TablePurposeKey columnsYour obligation
consent_recordsProof the member agreed before any PHI was storeduser_id, consent_type, granted, granted_by, ip_address, user_agent, scope jsonb, expires_at, revoked_atCheck before every write. Never bypass.
audit_logHIPAA-required trail of PHI accessuser_id, action, resource_type, resource_id, details jsonb, ip_address, created_atWrite a row on every create · read · update · sync.
data_requestsGDPR / CCPA export & deletion queueuser_id, request_type, status, requested_by, processed_byThe in-app account-deletion path must file here.

Consent types in use

ValueRegimeWhen required
health_data_collectionHIPAABefore any LIVE It Tracker write. Universal.
coppa_parentalCOPPAMember under 13. Verifiable parental consent.
educational_recordsFERPAMember in a school/IDEAS Challenge context.
gdpr_data_processingGDPREU members.
leaderboard_consentCOPPA / FERPAPlanned — before a member appears on any leaderboard.

Row-level security — the model you must not break

All 13 tables have RLS enabled with 59 policies in total. The shape is consistent:

-- Owner access (all four verbs, on every table)
CREATE POLICY "Users can view own pain entries" ON lit_pain_entries
  FOR SELECT TO authenticated
  USING (user_id = auth.uid()
     OR EXISTS (SELECT 1 FROM profiles
                 WHERE id = auth.uid()
                   AND role IN ('platform_admin','super_admin')));

-- Org-scoped read, on the 6 leaderboard-eligible tables only
CREATE POLICY "Org members can view weight for leaderboards" ON lit_weight_entries
  FOR SELECT TO authenticated
  USING (organization_id IN
        (SELECT organization_id FROM profiles WHERE id = auth.uid()));
Known issue you should be aware of Those org-scoped leaderboard policies grant read access to whole rows — including notes — to every member of the same organization. That is broader than a leaderboard needs. Tightening it (a view exposing only aggregates, or column-level grants) is on our compliance backlog and is Open Decision #9. Do not build a mobile feature that depends on the current breadth.
4

Compliance Non-Negotiables

Seven rules. None of them are style preferences.

Why this section exists On 5 April 2026 we ran a full compliance review of the LIVE It Trackers against HIPAA, FERPA, COPPA, GDPR and WCAG 2.1 AA. Seven gaps were found; four were closed in code that same day. Those four patches are load-bearing. A Flutter rewrite that reimplements the trackers natively without reimplementing these four controls would put us backwards, not forwards. Whatever you build must carry them.

Rule 1 · PHI never touches device storage while authenticated

The web app sets a PHI_SAFE_MODE flag the moment a Supabase session is confirmed. It then actively deletes nine known PHI keys from localStorage and blocks all future PHI writes there. Unauthenticated demo mode uses in-memory arrays only.

Your equivalent: no PHI in shared_preferences, Hive, sqflite, or any file that is not encrypted and excluded from backup. On iOS, health data must be excluded from iCloud backup — Apple prohibits storing personal health information in iCloud outright. If you want an offline cache, propose it explicitly with an encryption design; do not just add one.

Rule 2 · Every PHI touch writes an audit row

ffhAuditLog(action, resourceType, resourceId, details) fires after every successful save and on every load. Actions in use: create, read, update, sync.

Your equivalent: a health-platform sync is a PHI event. Reading 90 days of weight out of HealthKit and writing it to Supabase logs as sync, with the record count and source platform in details — and never the values themselves. The audit log is not a place to stash payloads.

Rule 3 · Consent gates the write, every time

On session start the app queries consent_records for an active health_data_collection grant. No grant → consentGranted = false → every save function refuses. A WCAG-compliant modal explains the collection and, on acceptance, writes the consent row before data can flow.

Your equivalent: the OS health permission prompt is not consent. Apple asking "share Health data with FFH?" answers a different question than "may FFH store your health data on its servers under these terms?" You need both, in that order: FFH consent first, OS permission second.

Rule 4 · 30-minute inactivity timeout

Monitors click, keypress, scroll, touchstart and debounced mousemove. Warning toast at 25 minutes with an Extend Session action (role="alert", aria-live="assertive"); at 30 minutes it calls auth.signOut(), purges in-memory PHI and shows a blocking session-expired overlay.

Your equivalent: mobile lifecycle is harder — background, resume, app switcher, and the OS screenshot taken when your app is backgrounded. Obscure PHI on AppLifecycleState.inactive, and treat time spent backgrounded as counting toward the timeout.

Rule 5 · Minors are the default assumption, not the edge case

A significant share of our membership is under 18, reached through schools, youth sports and community programs. Two documents govern this and both are binding on your build:

Age bandLeaderboards — internalLeaderboards — externalPresence / "playing now"
Under 13Only with active coppa_parental consentNever. No exception.Always hidden
13–17Allowed with org permissionAnonymised alias by defaultHidden by default
18+Allowed, user opt-out availableHidden by default on externalVisible internal, hidden external

User preference can always restrict further than the organization allows, and can never expand beyond it. A shared filter function — ffhFilterLeaderboardFields(playerData, scope, orgSettings, userAge, userPreference) — is the specified single point of enforcement. If the mobile app renders any leaderboard, it calls that same logic. It does not roll its own.

Current state All game leaderboards today use hardcoded demo arrays, so nothing is exposed yet. The filter function and the organization_settings table are designed but not yet built. If leaderboards are in your scope, this is net-new work for you and it must land before any real data reaches a leaderboard surface.

Rule 6 · Store-policy rules that constrain the code

RuleSourceWhat it forbids
No advertising useApple 5.1.3(i) & HealthKit docsHealth data may not be used for advertising, marketing or use-based data mining. You may show ads; you may not target them with health data. We do not run ads — keep it that way in code.
No third-party disclosureApple HealthKit docsNo sharing without express user permission, and even then only with parties who provide the user a health or fitness service. This rules out sending health data to a generic analytics or crash SDK.
No sale to brokersApple HealthKit docsAbsolute.
No iCloudApple 5.1.3(ii)Personal health information must not be stored in iCloud. Audit every local-storage plugin's directory choice and backup flags.
No false writesApple 5.1.3(ii)Do not write inaccurate or synthesised data into HealthKit. Demo/seed data must never reach the health store.
Usage strings mandatoryHealthKit frameworkNSHealthShareUsageDescription and NSHealthUpdateUsageDescription. Missing = crash on launch, not a warning.
Privacy policy, twiceApple 5.1.1(i) + HealthKitLinked in App Store Connect metadata and reachable inside the app. Ours is live at theforce.health/privacy. Same URL everywhere.
Account deletion in-appBoth storesRequired. Must file a data_requests row and actually cascade.

Rule 7 · Android Health Connect permission discipline

Google Fit is not an option — sign-ups closed 1 May 2024 and the APIs are being retired through 2026. Build against Health Connect (androidx.health.connect) only.

  • Every developer must complete the Health apps declaration form in Play Console (App content page) — even apps with no health features must declare that.
  • Each Health Connect data type must be declared and justified individually.
  • Blood pressure (READ_BLOOD_PRESSURE) is explicitly on Google's heightened-scrutiny list, alongside reproductive health, substance use and skin temperature. You must demonstrate a specific user-facing feature that needs it. Our justification is chronic-condition management.
  • READ_OXYGEN_SATURATION is not named in that list, but SpO₂ is governed as sensitive body-sensor data. Plan for a full justification anyway.
  • Two special permissions with their own approvals: READ_HEALTH_DATA_HISTORY (without it, reads are capped at 30 days) and READ_HEALTH_DATA_IN_BACKGROUND (required for any non-foreground sync). We want both. Budget for the review.
  • Request the minimum set. An unused permission in the manifest is a rejection risk with no upside.
Correction to an earlier internal document Our existing App Deployment Tracker cites 28 January 2026 as the effective date for the Play organization-account / D-U-N-S requirement for health apps. We could not substantiate that against Google's own policy pages — Google's 28 January 2026 announcement covers unrelated matters. The requirement is real and appears in Play Console Requirements, surfaced in the July 2026 policy updates with an effective date of 30 September 2026. Treat 30 September 2026 as the date, verify it yourself before you rely on it, and start D-U-N-S issuance immediately either way — D&B issuance plus Google verification is not same-day.

The five open go-live gates

Four of seven compliance gaps were closed in code on 5 April 2026. These five gates remain, and no real member PHI enters the system until all five are closed. Tick-state saves in your browser.

5

The Native Bridge Contract

The one interface both sides code against. This is the deliverable that unblocks everyone else.

Why a bridge at all

Our platform is web. Health data is native. The bridge is the seam. Our existing code already anticipates it — kardia-sdk-bridge.js shipped in June 2026 with function native() { return (window.FFHNative && window.FFHNative.kardia) || null; } and degrades to manual entry when it is absent. Our App Deployment Tracker names window.FFHNative.getHealth([...]) as the target.

What does not yet exist is a written specification. Producing and agreeing that spec is your first deliverable, before any tracker feature work. What follows is our proposed shape — argue with it, then we freeze it.

Proposed window.FFHNative surface

/* Injected by the Flutter shell into every WebView page, before first paint.
   Absent in a plain browser — every caller MUST degrade gracefully.
   All methods return Promises. All errors resolve to {ok:false, code, message}
   and never reject, so a missing capability can never crash a web page. */

window.FFHNative = {

  /* ── Capability discovery — call this first, always ── */
  platform()      // → 'ios' | 'android'
  version()       // → { app:'1.0.3', build:42, bridge:1 }  bridge = contract version
  capabilities()  // → ['health.read','health.write','ble.bp','push','camera','biometricUnlock']

  /* ── Health permissions ── */
  health.status(types)   // → { weight:'granted', bloodPressure:'denied', … }
                          // NOTE: iOS never reveals READ denial. 'granted' means asked, not authorised.
  health.request(types)  // → { ok, granted:[…], denied:[…] }  Shows the OS sheet. User gesture required.

  /* ── Health reads ── */
  health.read({
    types:  ['weight','steps','sleep'],  // FFH canonical names — see §6
    from:   '2026-05-01T00:00:00Z',        // ISO-8601, UTC
    to:     '2026-08-20T00:00:00Z',
    limit:  1000
  })
  // → { ok:true, samples:[ { type, value, unit, start, end,
  //                          source:{ name, bundleId }, uuid } ], truncated:false }
  // uuid = the platform's own sample identifier. This is the dedupe key. Non-negotiable.

  /* ── Health writes (only for member-entered data) ── */
  health.write({ type, value, unit, start, end, metadata })
  // → { ok:true, uuid:'…' }
  // NEVER call with demo, seed or estimated data — Apple 5.1.3(ii).

  /* ── Background sync ── */
  health.enableBackgroundSync(types)  // Android: needs READ_HEALTH_DATA_IN_BACKGROUND
  health.onUpdate(callback)           // iOS HKObserverQuery / Android changes API

  /* ── Bluetooth devices (see §7) ── */
  ble.scan({ service:'0x1810', timeoutMs:15000 })
  ble.connect(deviceId)
  ble.onReading(callback)
  kardia                               // present only when the AliveCor SDK is embedded

  /* ── Platform services ── */
  push.token()          // → FCM / APNs token for our notification service
  camera.scanBarcode()  // native scanner — replaces html5-qrcode in-app
  camera.capture({ purpose:'ingredient-label' })
  biometricUnlock()     // Face ID / fingerprint to re-enter after timeout
  secureStore.get/set   // Keychain / Keystore. Session tokens ONLY — never PHI.
}
Bridge rules — non-negotiable 1. The bridge moves health values and nothing else — no names, emails, member IDs or free-text notes cross it.
2. Every method degrades. window.FFHNative undefined must never break a page; that is how the site keeps working in a desktop browser.
3. Version the contract (bridge:1). Web and app ship on different cycles and an old app will meet new web code.
4. Nothing crossing the bridge is logged, in either direction, in any build configuration.
5. Origin-check every message. The WebView loads theforce.health only — an injected bridge reachable from an arbitrary origin is a PHI exfiltration path.
The iOS trap that catches everyone HealthKit never tells you that read access was denied. By design, a denied read type returns an empty result set, indistinguishable from "this member has no data." Do not build UI that says "no data found" and do not treat empty as authorised. Track whether you have asked, surface an explicit "check Health app settings" path, and never silently no-op.

Second trap: the HealthKit store is encrypted while the device is locked, so background reads can simply fail. Writes queue and flush on unlock. Your sync engine must tolerate both without dropping or double-writing.
6

Health Platform Field Map

Every syncable field, both platforms, with the traps marked.

Verify before you ship This map was compiled in August 2026 from Apple and Google's current developer documentation. Platform types change. Treat it as the agreed starting point and your specification baseline — not as a substitute for checking the SDK you actually compile against. Where we are uncertain, we say so in the row.
FFH fieldApple HealthKitAndroid Health ConnectUnitsDir.Bar
Weight
lit_weight_entries.value
HKQuantityTypeIdentifier
.bodyMass
WeightRecord.weightkg · lb↔Std
Height
BMI calculator input
.heightHeightRecord.heightm · cm · in↔Std
BMI
computed in-page
.bodyMassIndex
(dimensionless count)
No record type. Compute from weight + height.—iOS ↔
Android n/a
Std
Blood pressure
biometric_logs
.bloodPressureSystolic
.bloodPressureDiastolic
paired via HKCorrelationTypeIdentifier.bloodPressure
BloodPressureRecord
.systolic .diastolic
.bodyPosition .measurementLocation
mmHg↔HIGH BAR
SpO₂
biometric_logs
.oxygenSaturationOxygenSaturationRecord
.percentage
HK percent unit vs HC 0–100↔Justify
Heart rate — resting.restingHeartRateRestingHeartRateRecord
.beatsPerMinute
bpm↔Sensitive
Heart rate — avg / peak.heartRate
+ statistics query
HeartRateRecord.samples
[{time, beatsPerMinute}]
bpm↔Sensitive
Steps
lit_wellness_log.steps
.stepCountStepsRecord.countcount↔Std
Exercise minutes
lit_wellness_log
.exercise_minutes
.appleExerciseTime
(likely read-only)
No record. Derive from ExerciseSessionRecord durations.minutes← readStd
Active energy.activeEnergyBurnedActiveCaloriesBurnedRecord
.energy
kcal · kJ↔Std
Sleep hours
lit_wellness_log
.sleep_hours
HKCategoryTypeIdentifier
.sleepAnalysis
inBed · awake · asleepCore
· asleepDeep · asleepREM
SleepSessionRecord
.stages[] — LIGHT · DEEP
· REM · AWAKE
duration↔Std
Workout session
lit_workout_history
HKWorkout
+ HKWorkoutActivityType
ExerciseSessionRecord
.exerciseType .segments .laps
duration
+ energy
↔Std
Calories eaten.dietaryEnergyConsumedNutritionRecord.energykcal↔Std
Carbs · protein · fat · fiber · sugar.dietaryCarbohydrates
.dietaryProtein
.dietaryFatTotal
.dietaryFiber
.dietarySugar
NutritionRecord
.totalCarbohydrate .protein
.totalFat .dietaryFiber .sugar
grams↔Std
Sodium.dietarySodiumNutritionRecord.sodiumHK mg · our schema g↔Std
Water
water_glasses
.dietaryWaterHydrationRecord.volume
— separate from NutritionRecord
mL · L
we store glasses
↔Std
Body temperature.bodyTemperatureBodyTemperatureRecord°C · °F↔Std
skin temp is HIGH BAR — avoid
Pain levelNo equivalent on either platform. FFH-only. Do not improvise a mapping.0–10——
Treatment / medicationOut of scope. Do not touch clinical-records or medication APIs.———
Menstrual / reproductiveExplicitly out of v1 scope — highest scrutiny bar on both platforms, and we have no feature requiring it.——EXCLUDED

Five traps that will silently corrupt member data

Trap 1 · The SpO₂ 100× bug Apple documents oxygenSaturation as using "percent units" without stating whether 1.0 or 100 represents 100%. Health Connect's Percentage is unambiguously 0–100. Get this wrong and you write 0.98% or 9800% saturation into a member's health record. Verify empirically on a real device against a known reading before this ships. Add an assertion that rejects anything outside 70–100.
Trap 2 · Sodium units HealthKit's dietarySodium is a mass type conventionally handled in milligrams; our lit_nutrition_diary.sodium is numeric(6,1) in grams like every other macro. A missed 1000× conversion turns 2.3 g into 2300 g. Convert explicitly at the boundary and unit-test it.
Trap 3 · Water in glasses We store water_glasses as an integer count. Both platforms store a volume. "One glass" is not a unit — you must fix a conversion (we suggest 8 fl oz / 237 mL), write it down in the spec, and surface it to the member rather than burying it.
Trap 4 · Sleep model mismatch HealthKit stores discrete, overlapping samples — an inBed sample overlapping non-overlapping stage samples. Health Connect stores one session with nested stages. Naively summing HealthKit samples double-counts. And HealthKit's "core" is not exactly Health Connect's STAGE_TYPE_LIGHT — document the mapping you choose.
Trap 5 · The duplication spiral — the one that actually loses trust Member logs 180 lb in the web app → you write it to HealthKit → your next sync reads it back → you write it to Supabase again. Now they weigh 180 lb twice. Two weeks later they have 400 rows.

The fix is not clever conflict resolution. It is provenance. Every record needs to know where it came from, on both sides. Read the source bundle ID and skip anything our own app wrote. Store the platform sample uuid alongside our row so a re-read is recognisable. This needs a schema change (see Open Decision #5) — raise it before you write sync code, not after.
7

Devices & Biometrics

Phase 2. Partly scaffolded, partly blocked on partner agreements.

Dr. Rob Gillio, our co-founder, drives the device side. A biometrics module already exists at /ffh-biometrics/ with working pages for blood pressure, pulse oximetry, ECG and a universal vitals view. Readings land in a biometric_logs table using reading_type + a values JSONB payload, so new device types need no schema change. Member-uploaded images go to a private biometric-uploads Storage bucket, path-scoped to {user_id}/….

Device / classRouteStatus todayWhat you would build
Standard BLE BP cuff
GATT service 0x1810
Direct BLEScaffold + parser stubNative BLE central, GATT parse, expose via FFHNative.ble. Clinical/pro cuffs speak this; most consumer cuffs do not.
Pulse oximeter
GATT service 0x1822
Direct BLENot startedSame pattern. Nonin and similar are reachable.
Omron consumer cuffs
10-Series, EVOLV, Platinum
OMRON Connect Create cloud API (OAuth 2.0 / OIDC)Bridge written, NOT provisionedBlocked on a partner agreement. These do not speak open BLE — they pair only with the Omron app and sync to Omron's cloud. Client secret stays server-side in /api/omron/*; the client holds only the public client_id.
AliveCor KardiaMobile
single-lead ECG
Native SDK (license-gated) or enterprise data APIBridge written, manual fallback liveBlocked on an AliveCor agreement. No open GATT profile exists — the classic unit transmits by ultrasound into the microphone. A browser cannot read it. If we sign, the SDK lives in your shell and posts results to window.FFHNative.kardia.
Dr. Rob's custom modulesProprietaryProtocol notes neededGet these from Dr. Rob early — proprietary GATT parsing is the kind of thing that quietly consumes two weeks.
What ships without any agreement Manual entry on every device page today still awards coins, persists to Supabase and flows into the member's record. Nothing is blocked on partnerships — automation is an upgrade, not a prerequisite. Scope accordingly; do not put Omron or Kardia on a critical path you control.
The medical-device line Our footer states plainly: "This app is not a medical device and does not diagnose, treat, or prevent any condition." That line is a regulatory boundary, not marketing copy. Nothing you build may interpret a reading, flag a value as abnormal, generate an alert based on a clinical threshold, or recommend an action. Display, log, chart, trend — yes. Interpret — no. If a feature request drifts toward interpretation, escalate it to us rather than implementing it.
8

Built vs. To Build

An honest inventory, so you quote the right thing.

We maintain a live 32-step deployment tracker at /app-tracker/, phased 0–6 with an accountable owner per step. Bring it up in your kickoff and work from it. Its current picture:

PhaseAlready doneYours
0 · FoundationsPrivacy policy drafted and staged; bundle ID fixed at com.forceforhealth.ffh_appIn-app account-deletion path. Dev
1 · ShellFlutter scaffold and app_config.dart drafted, production URL presetBootstrap and verify every flow in-app (login, courses, community, checkout); app icon + splash; Firebase push; TestFlight + Android internal build. Dev
2 · Health & deviceshealth_service.dart skeleton; bridge concept; BLE scaffold + parser stubThe real work. HealthKit + Health Connect wiring, on-device permission flows, the bridge in §5, a visible Health surface in the web platform, device round-trip proof. Dev + Web
3 · Store prepApple App Privacy answers, Google Data Safety + Health declarations, and both store listings all draftedScreenshots — which need the built app first. Dev + CEO
4 · AppleInfo.plist usage strings and entitlements drafted; reviewer-notes angle draftedApp ID + HealthKit capability, merge plist/entitlements, archive and upload, working demo account. Dev
5 · GoogleHealth Connect permission justification draftedPlay App Signing and keystore custody, signed .aab, internal → closed → production, IARC rating. Dev + CEO
6 · Launch—Staged rollout, crash/ANR monitoring, update cadence, SDK currency. Dev

Recommended technical baseline

Health plugin
The health package (CACHET) is the only cross-platform Flutter option with a current maintenance cadence — v13.3.2, Aug 2026. Google Fit support removed, correctly.
iOS floor
iOS 15.0+ (the package's own floor).
Android
FlutterFragmentActivity, not FlutterActivity, on Android 14+. Health Connect <queries> block; every permission declared; activity-alias exposing the privacy policy.
Bundle / app ID
com.forceforhealth.ffh_app — permanent once submitted. Confirm before first upload.
Backend
Supabase, existing project. No new backend.

Evaluate and disagree freely — but disagree in writing, with reasoning, in your first-week technical response.

Explicitly out of scope for v1

  • Automated screen-time capture — Apple Family Controls / Android UsageStats. Manual entry stays.
  • Menstrual and reproductive health — highest scrutiny, no feature needs it.
  • Clinical records — Apple's Clinical Health Records API. Not now.
  • Any clinical interpretation — see the medical-device line in §7.
  • Native rewrite of the trackers — the trackers stay web for v1. Native is the health layer and the shell.
  • Leaderboard rendering — unless separately scoped, because the privacy filter is not built yet.
9

IP, Security & Contract Terms

What will be in your agreement. Stated now so nothing is a surprise later.

Our posture, in one sentence The Force for Health® Network licenses its intellectual property forward and does not assign it — and we expect the same rigour in the other direction: work we commission is assigned to us, and anything of yours that you bring must be disclosed in writing before it ships. This is not distrust. It is the same discipline we apply to ourselves, and it is what keeps a codebase licensable years later.

Work product & assignment

  • All code, configuration, build scripts, store assets and documentation produced under this engagement are work made for hire, assigned to FFH.
  • Store accounts stay in FFH's name. Signing certificates, provisioning profiles and the Android upload keystore are FFH property held in FFH custody — you get working access, not ownership. Losing the Android upload key permanently blocks all future updates, so key custody is a contract term, not a habit.
  • The bundle ID com.forceforhealth.ffh_app is ours and permanent.

Your pre-existing IP — disclose it up front

If you intend to bring a private package, an internal framework, a code generator or any component you created before this engagement, list it before you deliver code. We will paper a licence for it. We use exactly this instrument with our own clients — an itemised, dated Vendor IP Disclosure Schedule — because undisclosed embedded IP is indistinguishable from assigned work once it ships.

Third-party dependencies: supply a full licence manifest with your first build. No AGPL or SSPL, no exceptions. GPL and LGPL need written approval before you add them.

Security terms specific to PHI

TermWhat it means in practice
No production PHI on your machineYou develop against seeded synthetic data. You are never given a production database credential, a service-role key, or a member export. If a bug is only reproducible with real data, we reproduce it on our side and hand you the finding.
No service-role key in the clientEver, in any build flavour, including debug. RLS is the control. Anything needing elevation goes server-side and comes to us first.
No PHI in logs or crash reportsApplies to print, debugPrint, analytics events, Sentry/Crashlytics breadcrumbs and push notification bodies. Apple's third-party disclosure rule makes a crash SDK carrying health values a policy violation, not just bad practice.
Secrets never in the repositoryOmron and AliveCor client secrets live in server environment variables. The mobile client holds public identifiers only.
Subcontractors disclosedIf anyone else touches this code, we need to know who, and they are bound by the same terms. HIPAA follows the data, not the org chart.
Breach notificationSuspected exposure of member data is reported to FFH within 24 hours of discovery. Report it even if you are not sure. We would far rather investigate a false alarm.
NDA before repository accessStandard. Signed before the first clone.

Note on legal status: this section states our commercial and security position so you can price and plan against it. It is not the contract and it is not legal advice. The executed agreement is papered by counsel and it governs.

10

Open Decisions

Twelve things we have not settled. Your first deliverable is a recommendation on each.

How we would like these handled We are not asking you to guess our preference. We are asking for your recommendation with reasoning and a cost implication, in writing, in your first two weeks — before implementation starts. Several of these change the schema, and a schema change is far cheaper before the sync engine exists than after.
1 · WebView or native for the trackers?

Our assumption is WebView for v1 with a native health layer. Does that survive Apple 4.2 in your judgement, or does a specific tracker need to go native to make the case? Which one, and what does it cost?

2 · Which trackers sync, in which direction, in v1?

Weight and steps are obvious. Nutrition is high-effort. Sleep is messy. Give us a ranked v1 set with effort estimates.

3 · Unify the biometrics tables?

BP, HR and SpO₂ live in biometric_logs with a JSONB payload; the trackers use typed lit_* tables. Keep two models, or bring vitals into a typed table? Health-platform sync makes this decision matter.

4 · Where does sync run?

On-device in the Flutter app writing to Supabase directly, or a server-side job? On-device is simpler and preserves the RLS model; server-side handles background and multi-device better. Trade-off?

5 · Provenance columns — what shape?

We are convinced we need source and external_uuid on the syncable tables (see §6 Trap 5). Propose the exact DDL and the dedupe rule. This is likely the first schema change.

6 · Nutrition sync granularity

Our schema is per-food-item; Health Connect is per-meal; HealthKit is per-nutrient. Which is the sync unit, and do we accept lossy round-tripping?

7 · Workout timestamps

lit_workout_history has a date; both platforms need start and end instants. Add columns, or synthesise? We lean toward adding columns.

8 · Consent sequencing in the app

FFH consent before the OS health prompt, we believe — but where exactly in onboarding, and what happens when a member grants FFH consent then denies the OS prompt? Design the flow, including the iOS "you cannot tell it was denied" case.

9 · Leaderboards in the mobile app — in or out?

The privacy filter function and organization_settings table are designed but not built. If leaderboards are in scope, that build is yours and it gates any real data.

10 · Offline behaviour

Members in rural Arizona lose signal routinely. Do we support offline logging? If yes, encrypted-at-rest queue design, plus the Rule 1 and iCloud constraints — this is a real decision with real cost.

11 · Push notifications and PHI

Push is part of our Apple 4.2 defence. But a notification body is visible on a lock screen. Propose a content policy — our position is that no notification may contain a health value, a medication name or a symptom.

12 · Where is the existing Flutter scaffold?

Our tracker records a scaffold, app_config.dart, health_service.dart, a BLE stub and scripts/bootstrap.sh as drafted. Locate it, assess it honestly, and tell us whether it is a foundation or a starting sketch. We would rather hear "start over" than inherit something you do not trust.

11

Artifact Index

Everything referenced here, and where it lives.

ArtifactLocationWhat it is
LIVE It TrackersDeploy Ready/live-it-trackers/index.html
theforce.health/live-it-trackers/
The application itself. ~1.2 MB, ~13,800 lines. Read the Supabase integration block at the end first.
LIT schema migrationSupabase/025-live-it-tracker-tables.sql13 tables, indexes, 59 RLS policies. Not yet run in production — Gate 3.
Compliance & audit schemaSupabase/005-compliance-audit-tables.sqlconsent_records, audit_log, data_requests.
Compliance Check & Gameplan v1.0FFH-LIVE-It-Trackers-Compliance-Check-Gameplan.docx5 Apr 2026. The seven gaps, four patches, five gates. Required reading.
Leaderboard Privacy Advisory v1.0FFH-Leaderboard-Privacy-Compliance-Advisory.docx5 Apr 2026. The youth-protection architecture in Rule 5.
App Deployment TrackerDeploy Ready/app-tracker/
theforce.health/app-tracker/
Live 32-step tracker, phases 0–6, owner per step. Work from this.
Biometrics moduleDeploy Ready/ffh-biometrics/Omron and Kardia bridges, BP / pulse-ox / ECG / universal-vitals pages, Storage RLS SQL, partnership notes.
Platform standing rulesCLAUDE.md (shared folder root)Deploy workflow, nav standards, brand-asset paths, slug rules, deploy-ledger obligations. Read before touching any web page.
Privacy policyDeploy Ready/privacy/ → theforce.health/privacySame URL in both store listings and in-app. Do not fork it.

Your first two weeks — what we would like back

One last thing Everything in this document exists because members trust us with information about their pain, their weight, their medication and their kids. The compliance architecture is not bureaucracy laid over the product — it is the product, because a health platform that leaks is worth less than no platform at all. Build like that and you will fit here well.