ProgramTemplate is a studio’s reusable blueprint. A Program is a copy assigned to one trainee. Both keep the plan itself in a content JSON column, and both use ProgramType to say which kind of plan it is.
ProgramType
ProgramFolder
Table program_folders. A coloured folder that groups templates of one type.
Index:
(studioId, type).
ProgramTemplate
Table program_templates.
Indexes:
studioId, folderId.
Program
Table programs. A plan assigned to a trainee.
Indexes:
studioId, clientId, sourceTemplateId, (clientId, status), (clientId, createdAt), (studioId, status), (releasePending, releaseAt).
Relations: workoutLogs, exerciseSubstitutions.
A trainee can hold several programs of the same type at once. The trainee endpoints return a list.
ProgramStatus
The intake-review hold
A plan assigned from an onboarding form review can be held back until the coach sends it.
The
(releasePending, releaseAt) index is what the release sweeper scans. releaseResponseId is never cleared.
File plans
A plan is a file plan whenpdfUrl is set. The file is the whole plan.
typestaysTRAININGorNUTRITION, so every type-based query keeps working.contentis{}. A training file plan may carry exactly one thing:{ "meta": { "workoutsPerWeek": 3 } }.readFilePlanContent(content)inapps/core-api/src/modules/programs/plan-kind.tsreturnsnullfor any other payload, so a builder save can never turn a file plan into a half-regular one.- Readers must call
isFilePlan(plan)before normalizing. The normalizers fabricate one empty day for empty content, which would make a file plan look like an empty regular plan. PlanKindSourcerequires thepdfUrlkey in its type. A query that forgets to selectpdfUrlfails to compile.
pdfUrl can also be a link to a website. isPlanFileUrl accepts any http or https URL. On the trainee app filePlanSource(url) in mobile/src/features/plans/lib/planKind.ts decides how to open it: a path ending in .pdf (ignoring the query string and hash) is a PDF, anything else is a site link. On Android a PDF goes through Google’s viewer because the WebView cannot render one.
Metrics that would be derived from plan content are not collected for a file plan. FILE_PLAN_NOTE holds the sentence the app and coach views show in their place.
TraineeExerciseSubstitution
Table trainee_exercise_substitutions. A trainee swapping an exercise for a coach-approved alternative, kept for future workouts.
Unique on
(programId, rowId): one substitution per row.
This is deliberately a table and not a field on the row inside content. The coach’s editor rebuilds every row from a whitelist, and PATCH /programs/:id writes the whole content blob, so anything extra stored inside content is erased on the coach’s next save.
Substitutions are applied at read time. fromExerciseId is a staleness guard: if the row no longer prescribes that exercise, the coach has re-prescribed it and the substitution is ignored.
AutoFit identity keys in content.meta
Plans and templates written by the AutoFit import carry identity keys in content.meta so a re-sync finds the row it wrote: autofitTrainingId, autofitMenuId, autofitTemplateId, autofitProgramId, autofitWeek, autofitNutritionTemplateId, and autofitPdfId on file plans.
Both normalizers rebuild meta from a whitelist, so these keys are carried through explicitly by pickAutofitMeta, and keepAutofitMeta(content, stored) restores them when a save from an older builder drops them (apps/core-api/src/modules/program-templates/autofit-meta.ts). The import matches on the exact JSON value including its type, so ids stay strings and autofitWeek stays a number. See AutoFit import.