Skip to Content
Living documentation — last reviewed 2026-05-28
FeaturesBody MetricsBody Metrics — Behavior

Body Metrics — Behavior

State machine

Body metrics are single-shot records, not stateful. The lifecycle is:

create → (optional patch) → (optional soft-delete)

No publish, no review, no audit phases. Each row is a snapshot at recorded_at.

Invariants

InvariantEnforcement
One row per (membershipId, metricType, recordedAt, customLabel) — duplicates of “weight on 2026-05-28” collapseDB unique index body_metrics_unique_idx using COALESCE(custom_label, '')
metric_type='custom' requires a custom_label (semantic — service-layer enforcement TBD)Service layer / DTO
unit matches the metric type (weightkg/lb; body_fat%)DTO validator + enum constraints
Member can see only their own metrics; staff can see members in their orgrequireAccess helper in BodyMetricsService

Golden paths

Member self-reports weight

  1. Member opens the mobile body-metrics screen.
  2. Picks “Weight”, enters value, optional recorded_at (defaults to today).
  3. POST /organizations/:orgId/body-metrics/self → row inserted with recorded_by=user.id.
  4. UI optimistically updates the latest card and trend chart.

Coach records body fat on member’s profile

  1. Coach navigates to the member detail page → Metrics tab.
  2. Clicks ”+ Add measurement”, picks body_fat, enters value.
  3. POST /organizations/:orgId/members/:membershipId/metrics with recorded_by=coach.user.id.

Member edits an old entry

  1. Open the entry in the trend chart drill-down.
  2. PATCH /members/:membershipId/metrics/:id with new value.

Soft delete

  1. DELETE sets deleted_at; all reads filter on deleted_at IS NULL.

Edge cases

ScenarioBehavior
Two members of the same org weigh in on the same dayTwo rows, distinct membership_ids — unique constraint scopes to membership
Member adds “weight” twice on the same daySecond one trips the unique index — 409 or 500; UI should PATCH instead
Unit changed between measurements (kg → lb)Allowed at the row level; chart UI must convert client-side
custom metric with empty custom_labelDB unique treats it as ”; collides with another empty custom on the same day. Should be DTO-blocked
Coach views a member they’re not assigned torequireAccess only checks org membership and isStaffRole — any staff can view any member’s metrics

Side effects

ActionWrites
Createbody_metrics insert
Updatebody_metrics update
Soft deletebody_metrics update (deleted_at)
Settings updatebody_metric_settings upsert (one row per membership, enabled_metrics JSONB array)

No events emitted.

Permissions

ActionAuth
Self-reportActive org membership; recorded_by pinned to caller
Staff record on memberStaff role + same-org as the target membership
List / viewSelf or staff
Update / deleteSelf or staff