Body Metrics — Data Model
Schema: libs/db/src/lib/schema/body-metrics.ts.
body_metrics
| Column | Type | Notes |
|---|---|---|
id | uuid PK | |
membership_id | uuid NOT NULL | FK memberships(id) |
organization_id | uuid NOT NULL | FK organizations(id) |
metric_type | body_metric_type NOT NULL | weight, body_fat, chest, waist, hips, thigh, arm, custom |
value | numeric(10,2) NOT NULL | Stored as text in JS — parse with parseFloat |
unit | body_metric_unit NOT NULL | kg, lb, cm, in, % |
custom_label | varchar(100) | Required when metric_type='custom' (semantic) |
notes | text | |
recorded_at | date NOT NULL | Day granularity |
recorded_by | uuid | FK users(id) — null if system-generated |
created_at / updated_at / deleted_at | timestamptz | Standard soft-delete pattern |
Indexes:
- Unique on
(membership_id, metric_type, recorded_at, COALESCE(custom_label,''))— duplicate guard. body_metrics_membership_type_idxpartial on(membership_id, metric_type, recorded_at DESC)WHEREdeleted_at IS NULL— chart query.body_metrics_org_idxonorganization_id.
body_metric_settings
| Column | Type | Notes |
|---|---|---|
id | uuid PK | |
membership_id | uuid NOT NULL UNIQUE | One row per membership |
organization_id | uuid NOT NULL | |
enabled_metrics | jsonb NOT NULL DEFAULT [] | Array of metric type strings |
Multi-org isolation
- Rows carry both
membership_idANDorganization_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 filterdeleted_at IS NULL. - No hard delete path in current code.