6/8 - Public Beta (Discord)
|See Changelog
Skip to main content

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

MetricFormulas / MethodsNotes
Body Fat %Navy, YMCA, CUN-BAEUser preference picks default; all stored for auditing.
Lean Body MassBoer, James, HumeCompares multiple estimations for trend analysis.
Muscle MassMartin et al.Useful for hypertrophy-focused programs.
Basal Metabolic RateMifflin-St Jeor, Harris-BenedictDownstream TDEE hints provided.
Additional MetricsBMI, water %, visceral fat level, metabolic ageCalculated 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 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.

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: Includes bodyFatPercentage, 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 MeasurementPhotoType to mark relaxed vs. flexing shots.
  • Captures uploader user ID and expiresAt for 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 in src/services, GraphQL resolvers stay thin.
  • Run npm run seed:test-env before 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-storage for setup details.
  • Cover new DTOs with unit tests under tests/unit/services/measurements, mirroring the module path.