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.
How to read the rest of this document
| Section | What it gives you | Why you need it |
|---|---|---|
| 2 · What the Trackers Are | The ten trackers and what each one captures | Scope. Every one of these is a sync candidate. |
| 3 · Data Architecture | Full data dictionary for 13 lit_* tables + the RLS model | You will read and write these. Field names are exact. |
| 4 · Compliance | Seven hard rules, four shipped patches, five open gates | Break one of these and we cannot ship at all. |
| 5 · Bridge Contract | The window.FFHNative JS ⇄ Dart API you must implement | This is the actual interface spec. Build to it. |
| 6 · Field Map | Every tracker field mapped to HealthKit + Health Connect types | Removes the guesswork and the 100× unit bugs. |
| 7 · Devices | Omron, AliveCor Kardia, generic BLE profiles | Phase 2. Some need signed partner agreements. |
| 8 · Built vs. To Build | Honest inventory of what already exists | So you do not re-quote work that is done. |
| 9 · IP & Security | Assignment, disclosure, and no-PHI-on-your-machine rules | These will appear in your contract. No surprises. |
| 10 · Open Decisions | Twelve questions we have not answered yet | Your first deliverable is your recommendation on these. |
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.
| # | Tracker | What the member logs | Primary table | Sensitivity |
|---|---|---|---|---|
| 1 | Wellness / Daily Check-in | Mood, energy 1–5, sleep hours, water, steps, exercise minutes | lit_wellness_log | PHI |
| 2 | Weight & BMI | Weight with lbs/kg toggle, goal weight + target date, BMI calculator | lit_weight_entries lit_weight_goals | PHI |
| 3 | Pain | Body location, 0–10 severity, pain type, timestamp, notes | lit_pain_entries | PHI |
| 4 | Treatment Response | Treatment name + category, pain before/after, effectiveness 1–5, duration | lit_treatment_entries | PHI |
| 5 | Nutrition | Food diary by meal, macros, micros, barcode scan, calorie/macro goals | lit_nutrition_diary lit_nutrition_goals | PHI |
| 6 | Screen Time | Per-app minutes (JSONB), daily goal hours, trend + donut charts | lit_screen_time | Behavioural |
| 7 | Workouts | Saved routines with exercise library, sets/reps/rest; session history with XP | lit_workouts lit_workout_history | PHI |
| 8 | Reactions & Products | Personal-care/food products, OCR'd ingredient lists, symptoms, severity, safety engine | lit_reactions_products | PHI |
| 9 | SERVE It | Volunteer/service hours, dollars, units, beneficiary, impact score | lit_serve_it_entries | PII only |
| 10 | Challenges | Personal + monthly community challenges, streaks, milestones, calendar | lit_challenges | Behavioural |
biometric_logs table serves the Biometrics module. Unifying these is Open Decision #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 │
└──────────────────────────────────────────────────────────┘
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.
| Column | Type | Notes |
|---|---|---|
| id | uuid pk | gen_random_uuid() |
| user_id | uuid | → profiles(id) ON DELETE CASCADE |
| organization_id | uuid | → organizations(id). Drives leaderboard scope. |
| date | date | NOT NULL |
| mood | text | CHECK IN great · good · ok · rough · bad |
| energy | int | CHECK 1–5 |
| sleep_hours | numeric(3,1) | Sync candidate ← HealthKit / Health Connect sleep |
| water_glasses | int | Sync candidate ↔ HydrationRecord / dietaryWater |
| steps | int | Sync candidate ← stepCount / StepsRecord |
| exercise_minutes | int | Sync candidate ← appleExerciseTime / derive from sessions |
| notes | text | Free text — treat as PHI |
| coins_earned | int | Gamification. Default 0. |
| created_at / updated_at | timestamptz | Default 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 · Column | Type | Notes |
|---|---|---|
| weight_entries.date | date | NOT NULL. Indexed (user_id, date). |
| weight_entries.value | numeric(5,1) | NOT NULL |
| weight_entries.unit | text | CHECK IN lbs · kg — default lbs. Store the unit; never assume. |
| weight_entries.notes | text | |
| weight_goals.target_value | numeric(5,1) | NOT NULL |
| weight_goals.target_unit | text | CHECK IN lbs · kg |
| weight_goals.target_date | date | Nullable |
(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.
| Column | Type | Notes |
|---|---|---|
| datetime | timestamptz | NOT NULL, indexed. Note: timestamp not date. |
| location | text | NOT NULL — body region |
| level | int | CHECK 0–10 |
| pain_type | text | Free text |
| notes · coins_earned | text · int |
lit_treatment_entries PHI
Medication and treatment response — what was taken, and did it work.
| Column | Type | Notes |
|---|---|---|
| date | date | NOT NULL |
| name | text | NOT NULL — treatment/medication name |
| category | text | |
| pain_before · pain_after | int | Paired with the pain scale |
| effectiveness | int | CHECK 1–5 |
| duration · duration_unit | numeric · text | |
| notes · coins_earned | text · int |
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.
| Column | Type | Notes |
|---|---|---|
| date | date | NOT NULL |
| meal | text | CHECK IN breakfast · lunch · dinner · snack |
| food_name · brand | text | food_name NOT NULL |
| calories | numeric(6,1) | kcal |
| carbs · protein · fat · fiber · sodium · sugar | numeric(6,1) | grams — including sodium, which HealthKit expects in mg. Convert. |
| quantity · unit | numeric(5,2) · text | Default quantity 1 |
| goals.calories/carbs/protein/fat/fiber | int | Defaults 1520 / 190 / 95 / 50 / 25 |
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
| Column | Type | Notes |
|---|---|---|
| date | date | UNIQUE (user_id, date) — upsert target |
| app_data | jsonb | Default []. Array of per-app minute records. |
| goal_hours | numeric(3,1) | Default 4 |
| notes | text |
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.
| Column | Type | Notes |
|---|---|---|
| workouts.name · workout_type | text | name NOT NULL |
| workouts.exercises | jsonb | Default [] — sets, reps, rest per exercise |
| workouts.total_sets · est_minutes | int | |
| history.workout_id | uuid | → lit_workouts(id) ON DELETE SET NULL |
| history.date | date | NOT NULL |
| history.exercises_count · duration_minutes | int | |
| history.xp_earned · coins_earned | int | Gamification |
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.
| Column | Type | Notes |
|---|---|---|
| product_type | text | CHECK IN baseline · reaction |
| name · brand · category | text | name NOT NULL |
| ingredients | jsonb | Default [] — populated by OCR or barcode lookup |
| rating | int | Star rating |
| reaction · symptoms · severity | text | Clinical symptom data. |
| date · image_url | date · text | Image goes to Supabase Storage, not the row |
{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
| Column | Type | Notes |
|---|---|---|
| date · service_type | date · text | Both NOT NULL |
| label · category · who | text | who = beneficiary — may name a third party |
| minutes · units | int | Default 0 |
| dollars | numeric(10,2) | Default 0 |
| score · notes · coins_earned | text · text · int |
lit_challenges BEHAVIOURAL
| Column | Type | Notes |
|---|---|---|
| challenge_type · title | text | title NOT NULL |
| start_date · end_date | date | start_date NOT NULL |
| target_value · current_value | int | current_value default 0 |
| status | text | CHECK IN active · completed · abandoned. Default active. |
| coins_earned | int |
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
| Table | Purpose | Key columns | Your obligation |
|---|---|---|---|
| consent_records | Proof the member agreed before any PHI was stored | user_id, consent_type, granted, granted_by, ip_address, user_agent, scope jsonb, expires_at, revoked_at | Check before every write. Never bypass. |
| audit_log | HIPAA-required trail of PHI access | user_id, action, resource_type, resource_id, details jsonb, ip_address, created_at | Write a row on every create · read · update · sync. |
| data_requests | GDPR / CCPA export & deletion queue | user_id, request_type, status, requested_by, processed_by | The in-app account-deletion path must file here. |
Consent types in use
| Value | Regime | When required |
|---|---|---|
| health_data_collection | HIPAA | Before any LIVE It Tracker write. Universal. |
| coppa_parental | COPPA | Member under 13. Verifiable parental consent. |
| educational_records | FERPA | Member in a school/IDEAS Challenge context. |
| gdpr_data_processing | GDPR | EU members. |
| leaderboard_consent | COPPA / FERPA | Planned — 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()));
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.Compliance Non-Negotiables
Seven rules. None of them are style preferences.
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 band | Leaderboards — internal | Leaderboards — external | Presence / "playing now" |
|---|---|---|---|
| Under 13 | Only with active coppa_parental consent | ||
| 13–17 | Allowed with org permission | Anonymised alias by default | Hidden by default |
| 18+ | Allowed, user opt-out available | Hidden by default on external | Visible 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.
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
| Rule | Source | What it forbids |
|---|---|---|
| No advertising use | Apple 5.1.3(i) & HealthKit docs | Health 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 disclosure | Apple HealthKit docs | No 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 brokers | Apple HealthKit docs | Absolute. |
| No iCloud | Apple 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 writes | Apple 5.1.3(ii) | Do not write inaccurate or synthesised data into HealthKit. Demo/seed data must never reach the health store. |
| Usage strings mandatory | HealthKit framework | NSHealthShareUsageDescription and NSHealthUpdateUsageDescription. Missing = crash on launch, not a warning. |
| Privacy policy, twice | Apple 5.1.1(i) + HealthKit | Linked in App Store Connect metadata and reachable inside the app. Ours is live at theforce.health/privacy. Same URL everywhere. |
| Account deletion in-app | Both stores | Required. 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_SATURATIONis 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) andREAD_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.
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.
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. }
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.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.
Health Platform Field Map
Every syncable field, both platforms, with the traps marked.
| FFH field | Apple HealthKit | Android Health Connect | Units | Dir. | Bar |
|---|---|---|---|---|---|
| Weight lit_weight_entries.value | HKQuantityTypeIdentifier .bodyMass | WeightRecord.weight | kg · lb | ↔ | Std |
| Height BMI calculator input | .height | HeightRecord.height | m · 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 HKCorrelationType | BloodPressureRecord .systolic .diastolic .bodyPosition .measurementLocation | mmHg | ↔ | |
| SpO₂ biometric_logs | .oxygenSaturation | OxygenSaturationRecord .percentage | HK percent unit vs HC 0–100 | ↔ | Justify |
| Heart rate — resting | .restingHeartRate | RestingHeartRateRecord .beatsPerMinute | bpm | ↔ | Sensitive |
| Heart rate — avg / peak | .heartRate + statistics query | HeartRateRecord.samples [{time, beatsPerMinute}] | bpm | ↔ | Sensitive |
| Steps lit_wellness_log.steps | .stepCount | StepsRecord.count | count | ↔ | Std |
| Exercise minutes lit_wellness_log .exercise_minutes | .appleExerciseTime (likely read-only) | No record. Derive from ExerciseSessionRecord durations. | minutes | ← read | Std |
| Active energy | .activeEnergyBurned | ActiveCaloriesBurnedRecord .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 | .dietaryEnergyConsumed | NutritionRecord.energy | kcal | ↔ | Std |
| Carbs · protein · fat · fiber · sugar | .dietaryCarbohydrates .dietaryProtein .dietaryFatTotal .dietaryFiber .dietarySugar | NutritionRecord .totalCarbohydrate .protein .totalFat .dietaryFiber .sugar | grams | ↔ | Std |
| Sodium | .dietarySodium | NutritionRecord.sodium | HK mg · our schema g | ↔ | Std |
| Water water_glasses | .dietaryWater | HydrationRecord.volume — separate from NutritionRecord | mL · L we store glasses | ↔ | Std |
| Body temperature | .bodyTemperature | BodyTemperatureRecord | °C · °F | ↔ | Std skin temp is HIGH BAR — avoid |
| Pain level | No equivalent on either platform. FFH-only. Do not improvise a mapping. | 0–10 | — | — | |
| Treatment / medication | Out of scope. Do not touch clinical-records or medication APIs. | — | — | — | |
| Menstrual / reproductive | Explicitly out of v1 scope — highest scrutiny bar on both platforms, and we have no feature requiring it. | — | — | ||
Five traps that will silently corrupt member data
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.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.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.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.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.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 / class | Route | Status today | What you would build |
|---|---|---|---|
| Standard BLE BP cuff GATT service 0x1810 | Direct BLE | Scaffold + parser stub | Native BLE central, GATT parse, expose via FFHNative.ble. Clinical/pro cuffs speak this; most consumer cuffs do not. |
| Pulse oximeter GATT service 0x1822 | Direct BLE | Not started | Same 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 provisioned | Blocked 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 API | Bridge written, manual fallback live | Blocked 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 modules | Proprietary | Protocol notes needed | Get these from Dr. Rob early — proprietary GATT parsing is the kind of thing that quietly consumes two weeks. |
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:
| Phase | Already done | Yours |
|---|---|---|
| 0 · Foundations | Privacy policy drafted and staged; bundle ID fixed at com.forceforhealth.ffh_app | In-app account-deletion path. Dev |
| 1 · Shell | Flutter scaffold and app_config.dart drafted, production URL preset | Bootstrap and verify every flow in-app (login, courses, community, checkout); app icon + splash; Firebase push; TestFlight + Android internal build. Dev |
| 2 · Health & devices | health_service.dart skeleton; bridge concept; BLE scaffold + parser stub | The 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 prep | Apple App Privacy answers, Google Data Safety + Health declarations, and both store listings all drafted | Screenshots — which need the built app first. Dev + CEO |
| 4 · Apple | Info.plist usage strings and entitlements drafted; reviewer-notes angle drafted | App ID + HealthKit capability, merge plist/entitlements, archive and upload, working demo account. Dev |
| 5 · Google | Health Connect permission justification drafted | Play 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
healthpackage (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, notFlutterActivity, 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.
IP, Security & Contract Terms
What will be in your agreement. Stated now so nothing is a surprise 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_appis 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
| Term | What it means in practice |
|---|---|
| No production PHI on your machine | You 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 client | Ever, 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 reports | Applies 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 repository | Omron and AliveCor client secrets live in server environment variables. The mobile client holds public identifiers only. |
| Subcontractors disclosed | If 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 notification | Suspected 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 access | Standard. 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.
Open Decisions
Twelve things we have not settled. Your first deliverable is a recommendation on each.
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?
Weight and steps are obvious. Nutrition is high-effort. Sleep is messy. Give us a ranked v1 set with effort estimates.
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.
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?
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.
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?
lit_workout_history has a date; both platforms need start and end instants. Add columns, or synthesise? We lean toward adding columns.
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.
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.
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.
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.
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.
Artifact Index
Everything referenced here, and where it lives.
| Artifact | Location | What it is |
|---|---|---|
| LIVE It Trackers | Deploy 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 migration | Supabase/025-live-it-tracker-tables.sql | 13 tables, indexes, 59 RLS policies. Not yet run in production — Gate 3. |
| Compliance & audit schema | Supabase/005-compliance-audit-tables.sql | consent_records, audit_log, data_requests. |
| Compliance Check & Gameplan v1.0 | FFH-LIVE-It-Trackers-Compliance-Check-Gameplan.docx | 5 Apr 2026. The seven gaps, four patches, five gates. Required reading. |
| Leaderboard Privacy Advisory v1.0 | FFH-Leaderboard-Privacy-Compliance-Advisory.docx | 5 Apr 2026. The youth-protection architecture in Rule 5. |
| App Deployment Tracker | Deploy Ready/app-tracker/ theforce.health/app-tracker/ | Live 32-step tracker, phases 0–6, owner per step. Work from this. |
| Biometrics module | Deploy Ready/ffh-biometrics/ | Omron and Kardia bridges, BP / pulse-ox / ECG / universal-vitals pages, Storage RLS SQL, partnership notes. |
| Platform standing rules | CLAUDE.md (shared folder root) | Deploy workflow, nav standards, brand-asset paths, slug rules, deploy-ledger obligations. Read before touching any web page. |
| Privacy policy | Deploy Ready/privacy/ → theforce.health/privacy | Same URL in both store listings and in-app. Do not fork it. |