Measurements & Body Composition
Why It Matters
- Single source of truth: Manual readings, lab imports, automatic calculators, and photos live in one entry so athletes never reconcile spreadsheets again.
- Coach-ready context: Notes, equipment tags, and goal pacing let remote coaches review the same evidence an in-person coach would have in a notebook.
- Automatic math, zero friction: Once an entry lands, the service runs the Navy/YMCA/CUN-BAE stack, lean-mass methods, BMR equations, and total daily guidance without user work.
- Media-aware history: Photo uploads flow through the secure progress-photo service and are linked back to measurements for visual trend reviews.
- GraphQL-first: Every feature exposed here (goals, entries, calculated payloads) is fully scriptable for dashboards, automation, or QA scenarios.
Headline Capabilities
Goal Contracts
- Targets are scoped per user and metric (
measurementType) so Sam cannot chase two waist goals at the same time. - Each goal stores start value, target value/date, priority, completion percentage, on-track flag, projected finish, and achievement history.
- Live telemetry includes streak status (“on pace,” “behind,” “complete”), so dashboards surface at-risk metrics automatically.
- Goals respect manual overrides—if Sam imports a DEXA reading, the goal immediately recalculates progress using that value.
Measurement Entries
- Entries accept weight, full circumference panels (waist/hips/chest/thigh/etc.), custom units per field, body-comp payloads, context fields, and notes.
- Auto-conversion normalizes units (lbs ↔ kg, inches ↔ cm) against the user profile but still stores raw input for audits.
- Each entry stores derived metrics (body fat, lean/muscle mass, BMI, BMR, water %, visceral fat, metabolic age) plus manual overrides.
- Entries reference encrypted photo keys for frontal, side, back, or pose shots. Signed URLs are issued on demand, keeping the raw assets private.
- Context captures location, instrumentation (tape vs. scan), hydration state, or anything relevant for coaches.
Automatic Calculations & Formulas
| Metric | Formulas / Methods | Notes |
|---|---|---|
| Body Fat % | Navy, YMCA, CUN-BAE | User preference picks default; all stored for auditing. |
| Lean Body Mass | Boer, James, Hume | Compares multiple estimations for trend analysis. |
| Muscle Mass | Martin et al. | Useful for hypertrophy-focused programs. |
| Basal Metabolic Rate | Mifflin-St Jeor, Harris-Benedict | Downstream TDEE hints provided. |
| Additional Metrics | BMI, water %, visceral fat level, metabolic age | Calculated automatically but can be overridden. |
Photo & Media Pipeline
- Upload endpoints categorize photos via
MeasurementPhotoType; entries only store opaque keys. - Viewing occurs through short-lived signed URLs, so Sam and coach Alex can review progress without exposing the raw storage bucket.
- API supports multi-angle uploads per entry plus metadata (lighting, pose cues) for future computer-vision tooling.
Feature Walkthrough
- Sam's Weekly Routine
- DEXA & Clinic Imports
- Sam + Coach Alex
Sam weighs in every Saturday. He records weight in pounds, waist/hips in inches, and a quick note about hydration.
- Entry auto-converts to his default metric profile, stores raw values, and runs all body-fat calculators.
- Because the rule
auto_calculate=true, the Navy method becomes the default but YMCA/CUN-BAE live in the payload. - Goal pacing updates immediately: “Waist to 78 cm – 4.5 cm remaining, projected 9 weeks.”
- Sam snaps front/side photos in the mobile uploader; keys are attached and viewable inside the same timeline.
Sam visits a clinic for a DEXA scan.
- He records a new entry with the scan’s weight, body fat %, lean mass, and metabolic age, marking each field as
manualOverride. - Automated calculations still run but get flagged as secondary so coaches see both “Clinic Value” and “OpenLift Estimate.”
- Goals treat the DEXA values as canonical for that day, preserving the integrity of the progress bar.
Coach Alex reviews every Monday.
- Dashboard shows Sam’s latest waist/lean mass trend arrows plus streak status for each goal.
- Alex can filter by context (“clinic”) or tags (post-flight) to understand anomalies.
- When Alex pastes notes (“DEXA Week 8 – clinic override”), they live with the entry and sync to coaching reports.
- Side-by-side photo comparisons pull directly from the measurement entry’s keys so no extra uploads are required.
Data Model Highlights
Measurement Goal Fields
id: Stable identifier for audits.measurementType: String enum (e.g.,waist,body_fat_percentage,lean_mass).targetValue,targetDate,priority: Primary contract knobs.startValue,currentValue,progressPercent,onTrack,projectedCompletionDate.statusHistory: Snapshot trail for compliance and reporting.
Measurement Entry Payload
measuredAt: ISO timestamp (UTC).weight: Value + unit; normalized weight stored separately.circumferences: Object of arbitrary sites with per-field units.bodyComposition: IncludesbodyFatPercentage,leanBodyMass,muscleMass,bmr,waterPercentage, etc.manualOverrides: Flags indicating which fields came from lab equipment.context: Location, equipment, mood, hydration, or other structured metadata.progressPhotoData: Keys for each angle plus optional tags (lighting,pose).notes: Markdown-safe text for athletes/coaches.
Progress Photo Metadata
frontalPhotoFileKey,sidePhotoFileKey,backPhotoFileKey,posePhotoFileKey.- Optional
MeasurementPhotoTypeto mark relaxed vs. flexing shots. - Captures uploader user ID and
expiresAtfor signed URLs.
GraphQL Reference
Create a Measurement Goal
mutation CreateMeasurementGoal($input: CreateMeasurementGoalInput!) {
createMeasurementGoal(input: $input) {
id
measurementType
priority
targetValue
targetDate
startValue
progressPercent
onTrack
projectedCompletionDate
}
}
Example variables (Sam targeting a 78 cm waist):
{
"input": {
"measurementType": "waist",
"targetValue": 78,
"targetDate": "2025-11-01T00:00:00.000Z",
"priority": "HIGH",
"startValue": 82.5
}
}
Create a Measurement Entry
mutation CreateMeasurementEntry($input: CreateMeasurementEntryInput!) {
createMeasurementEntry(input: $input) {
id
measuredAt
unitSystem
weight
bodyComposition {
bodyFatPercentage
leanBodyMass
muscleMass
bmr
}
circumferences { waist hips chest unit }
context { location equipment hydration }
manualOverrides
progressPhotos { frontalPhotoUrl sidePhotoUrl }
notes
}
}
Example payload mixing raw data and overrides:
{
"input": {
"weight": 178.5,
"circumferences": { "waist": 82.5, "hips": 98.1, "unit": "CM" },
"bodyComposition": { "bodyFatPercentage": 15.2, "bmr": 1890 },
"measuredAt": "2025-09-10T13:00:00.000Z",
"context": { "location": "Home", "equipment": "Tape", "hydration": "normal" },
"progressPhotoData": { "frontalPhotoFileKey": "sam/2025-09-10/front.jpg" },
"notes": "Post-flight, mild water retention"
}
}
Query Measurement History
query MeasurementHistory($input: MeasurementHistoryInput!) {
measurementHistory(input: $input) {
measuredAt
weight
bodyComposition {
bodyFatPercentage
leanBodyMass
muscleMass
bmr
waterPercentage
}
circumferences { waist hips chest shoulders unit }
progressPhotos { frontalPhotoUrl sidePhotoUrl }
manualOverrides
notes
}
}
Typical query variables:
{
"input": {
"limit": 5,
"includePhotos": true,
"order": "DESC"
}
}
Sample Response
{
"data": {
"measurementHistory": [
{
"measuredAt": "2025-09-10T13:00:00.000Z",
"weight": 178.5,
"bodyComposition": {
"bodyFatPercentage": 15.2,
"leanBodyMass": 151.5,
"muscleMass": 74.2,
"bmr": 1890,
"waterPercentage": 58.1
},
"circumferences": {
"waist": 32.5,
"hips": 38.6,
"chest": 41.0,
"shoulders": 48.3,
"unit": "INCHES"
},
"progressPhotos": {
"frontalPhotoUrl": "https://assets.openlift.dev/temp/sam-front.jpg",
"sidePhotoUrl": "https://assets.openlift.dev/temp/sam-side.jpg"
},
"manualOverrides": ["bodyComposition.bmr"],
"notes": "Post-flight, mild water retention"
}
]
}
}
Implementation & Testing Notes
- Measurements service leans on the same Universal Service Architecture: validations in
src/config, DTOs insrc/services, GraphQL resolvers stay thin. - Run
npm run seed:test-envbefore integration tests that rely on real measurement history. - The progress-photo pipeline depends on the object storage service; local dev uses signed URL mocks—see
docs/features/object-storagefor setup details. - Cover new DTOs with unit tests under
tests/unit/services/measurements, mirroring the module path.