Skip to Content
Living documentation — last reviewed 2026-05-28
FeaturesWorkout AssignmentsWorkout assignments — data model

Workout assignments — data model

Schema in libs/db/src/lib/schema/workouts.ts.

workout_assignments

ColumnTypeMeaning
iduuid PK
organization_iduuid FK → organizations.id, not nullDenormalized — saves a join through the workout on coach-overview / week-grid queries.
program_iduuid FK → programs.id nullableOptional — personal ad-hoc assignments may have no program.
user_iduuid FK → users.id, not nullThe athlete.
kindenum assignment_kind default 'workout'FIT-159. workout / rest / note.
workout_iduuid FK → workouts.id nullableLibrary workout. Null for rest/note.
snapshot_workout_iduuid FK → workouts.id nullableFIT-152 lazy-fork pointer. Equals workout_id until first fork. Null for rest/note.
notetext nullableRequired for kind='note'.
datedate (string mode), not nullLocal date.
sort_orderinteger default 0For multiple slots on the same day.
publishedboolean default falseTrue = visible to the member.
publish_attimestamptz nullableMorning-of drip target time. Worker flips published at this moment.
statusenum assignment_status default 'assigned'assigned/completed/skipped.
completed_attimestamptz nullableSet when status flips to completed.
assigned_byuuid FK → users.id, not nullThe coach who created the row.
created_at / updated_at / deleted_attimestampsSoft delete.

Indexes:

  • workout_assignments_user_date_idx on (user_id, date) WHERE deleted_at IS NULL — athlete week/day reads.
  • workout_assignments_program_date_idx on (program_id, date) WHERE program_id IS NOT NULL AND deleted_at IS NULL — coach week grid.
  • workout_assignments_org_date_idx on (organization_id, date) WHERE deleted_at IS NULL.
  • workout_assignments_workout_id_idx on workout_id — “who has Fran assigned this week?”.
  • workout_assignments_snapshot_workout_id_idx on snapshot_workout_id — used by forkSnapshotIfNeeded idempotency check.
  • workout_assignments_user_status_date_idx on (user_id, status, date) — next-due / last-completed.

CHECK constraints:

  • workout_assignments_kind_payload_chk
    • kind='workout'workout_id NOT NULL AND snapshot_workout_id NOT NULL.
    • kind='rest' ⇒ both null.
    • kind='note' ⇒ both null AND note NOT NULL.

workout_feed_posts

Lives in libs/db/src/lib/schema/scheduling.ts. One row per feed broadcast.

ColumnTypeMeaning
iduuid PK
program_iduuid FK → programs.id not nullMust be feed-mode.
workout_iduuid FK → workouts.id not nullLibrary workout.
snapshot_workout_iduuid FK → workouts.id nullableLazy-fork pointer for feed posts.
publish_attimestamptz not nullWhen the post becomes live.
notetext nullable
created_byuuid FK → users.id
timestamps + deleted_atSoft delete.

Indexes: workout_feed_posts_program_id_idx, workout_feed_posts_publish_at_idx.

Row lifecycle

EventDB effect
assignPersonalN inserts (one per athlete) with pointer state. Push notifications fire for published=true rows.
assignFeed1 insert into workout_feed_posts.
assignScheduleN inserts into class_sessions + class_session_workouts (in transaction).
applyToCoaching (template)Bulk insert into workout_assignments with published=false. No push.
updateUPDATE in place.
removeSoft delete (deleted_at = now()).
complete / skipStatus + completed_at UPDATE. Bound to the current member.
forkSnapshotIfNeededInside a FOR UPDATE txn: deep-copy workouts (+ sections + movements + comments) → new snapshot row; UPDATE snapshot_workout_id on the assignment.
setPublishedUPDATE published bool on listed ids.
bulkDeleteUPDATE deleted_at = now() on every match. INSERT one audit_logs row keyed by a new batch_id.
bulkPublishUPDATE published = true on every draft match. One audit row per call.

Soft-delete behavior

  • Soft delete only. workout_results and personal_records may reference workout_assignments.id indefinitely.
  • All hot queries filter deleted_at IS NULL.
  • Bulk delete soft-deletes; no cascade to results.

Multi-org isolation

  • organization_id is denormalized on the assignment row for every cross-row scan.
  • The coach-overview, bulk-preview, and findByWeek queries all start with eq(workoutAssignments.organizationId, orgId).
  • The bulk-filter resolver intersects with program_enrollments to include ad-hoc personal rows that are still on enrolled members’ calendars (FIT-201).
  • Members are gated against their own userId in findMyOne / findMyWeek / findMyToday.