Skip to Content
Living documentation — last reviewed 2026-05-28
FeaturesDaily ProgrammingDaily programming — data model

Daily programming — data model

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

daily_programming

ColumnTypeMeaning
iduuid PK
class_type_iduuid FK → class_types.id, not nullThe slot’s class type.
datedate (string mode), not nullThe slot’s date.
workout_iduuid FK → workouts.id, not nullThe library workout. Snapshots are not used here (broadcast surface).
sort_orderinteger default 0When multiple workouts on the same slot.
statusvarchar(20) default 'draft'draft / published. Not a DB enum; service / DTO enforces values.
created_byuuid FK → users.id, not null
created_at / updated_attimestampsNo deleted_at — deletes are physical.

Indexes / constraints:

  • UNIQUE (class_type_id, date, workout_id). Prevents the same library workout being added twice to the same slot. (Implementation in the migration; visible in the schema as unique().on(table.classTypeId, table.date, table.workoutId).)

There is no (class_type_id, date)-only index — the hot query is the week range scan, which uses date ordering directly. As the table grows, adding (class_type_id, date) may be worthwhile.

Row lifecycle

EventDB effect
PUT /daily-programmingInside a transaction: DELETE rows matching (class_type_id, date), then INSERT new rows for the supplied workouts. Status defaults to draft.
PATCH /statusUPDATE status for all rows matching (class_type_id, date).
DELETE /daily-programmingPhysical DELETE for (class_type_id, date). No tombstone.
applyToSchedule (template)Bulk INSERT (one row per workout cell that lands on an active day). Always status='draft'.

Soft-delete vs hard-delete

This is the only feature in the workouts domain that uses physical delete. There is no deleted_at column. Removing a slot is destructive; previous content is not recoverable through the API. (Audit-log instrumentation would be the next step if accountability becomes a requirement.)

Multi-org isolation

  • No organization_id column. Org scoping is done via class_types.programId → programs.organizationId.
  • getOrgClassTypeIds(orgId) resolves the org’s class type ids in one SQL join and then WHERE class_type_id IN (...) scopes the read.
  • Every write goes through verifyClassTypeBelongsToOrg, which independently re-verifies the class type’s org. Cross-org writes are blocked at the API boundary.
  • workoutIds are independently verified against workouts.organizationId = orgId.
  • Members’ visibility filter (only status='published') is applied JS-side after the SQL fetch.