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

Body Metrics — Data Model

Schema: libs/db/src/lib/schema/body-metrics.ts.

body_metrics

ColumnTypeNotes
iduuid PK
membership_iduuid NOT NULLFK memberships(id)
organization_iduuid NOT NULLFK organizations(id)
metric_typebody_metric_type NOT NULLweight, body_fat, chest, waist, hips, thigh, arm, custom
valuenumeric(10,2) NOT NULLStored as text in JS — parse with parseFloat
unitbody_metric_unit NOT NULLkg, lb, cm, in, %
custom_labelvarchar(100)Required when metric_type='custom' (semantic)
notestext
recorded_atdate NOT NULLDay granularity
recorded_byuuidFK users(id) — null if system-generated
created_at / updated_at / deleted_attimestamptzStandard soft-delete pattern

Indexes:

  • Unique on (membership_id, metric_type, recorded_at, COALESCE(custom_label,'')) — duplicate guard.
  • body_metrics_membership_type_idx partial on (membership_id, metric_type, recorded_at DESC) WHERE deleted_at IS NULL — chart query.
  • body_metrics_org_idx on organization_id.

body_metric_settings

ColumnTypeNotes
iduuid PK
membership_iduuid NOT NULL UNIQUEOne row per membership
organization_iduuid NOT NULL
enabled_metricsjsonb NOT NULL DEFAULT []Array of metric type strings

Multi-org isolation

  • Rows carry both membership_id AND organization_id. The denormalized org id allows org-scoped reads without a join.
  • Cross-org queries impossible at the API layer — every route validates membership.organizationId === :orgId.

PII handling

  • Weight, body fat, circumferences = sensitive health data.
  • Plaintext in DB; no field-level encryption.
  • Visible to the member and any staff in the same org. No selective access for, e.g., a specific coach only.

Soft / hard delete

  • Soft via deleted_at. All reads filter deleted_at IS NULL.
  • No hard delete path in current code.