Program.content or ProgramTemplate.content when type is NUTRITION. Like the training plan, the contract is a TypeScript type and a tolerant normalizer, not a Zod schema.
Macros are not stored in the plan. Items reference foods by id and quantities, and every reader computes calories and macros from the referenced FoodLibraryItem values (merged with the studio’s FoodLibraryOverride).
Where the contract lives
Plan shape
normalizeNutritionContent always returns schemaVersion: 1.
meta
days[]
A plan with no days normalizes to one empty day.
Days rotate around the trainee’s current day.
nutritionDayIndexForOffset(dayCount, offset) maps a whole-day distance from today to a day index, wrapping around. Today is always the first day and tomorrow the second.
days[].targets
Targets live on each day. The top-level targets key is the legacy shape: when present it seeds every day that has no value of its own.
The defaults are
DEFAULT_NUTRITION_TARGETS. On imported content the fallback is all zeros, so a missing target stays visibly missing.
days[].meals[]
items[]
items[].alts[]
Portions (MBP)
Perform can express a plan in portions instead of grams. A portion of a macro is a fixed number of calories, the studio’s anchor.roundMbp(x) snaps a portion count to the 0.05 grid and returns 0 for anything not finite and positive. Use it for every stored or displayed portion value.
Anchors are per studio and are read from Studio.settings by readMbpAnchors. Changing an anchor re-prices the food library, which is why a value outside the bounds is refused. See Studio settings JSON.
A food’s portions come from its macroType (protein, carb or fat) and its exchange quantities (proteinExchangeQty, carbExchangeQty, fatExchangeQty), or from its servingTiers. Those columns are described in Exercise and food libraries.
When a plan has no explicit portion targets, mbpTargets(targets, anchors) converts the gram targets through the anchors, each macro through its own.
Alternatives
There are three sources of substitutes for a plan item:items[].alts[], picked by the coach.- Automatic alternatives, when
meta.autoAltsis true. The backend computes them inapps/core-api/src/modules/trainee/food-alternatives.tsfrom foods of the samemacroType. - The trainee’s own swap for one day, stored in the day snapshot’s
swapsmap.
showAsAlternative = false is never offered as an alternative anywhere. A food the trainee excluded through Client.foodPreferences is filtered at read time.
Client.foodPreferences
Validated by foodPreferencesBody in apps/core-api/src/modules/clients/clients.schema.ts:
NutritionDayLog.data
One row per trainee per day, unique on (clientId, date). The API accepts any object (saveNutritionDayBody.data is a record of unknown values), so the app owns the shape and the backend parses it defensively with parseDaySnapshot.
<dateKey>__<id>. The parser strips the date prefix with idOf, so on the server each map is keyed by the plain item or meal id.
Today and yesterday are editable in the app. Older days show the saved snapshot read-only.
MealLog.items and FavoriteMeal.items
logMealBody.items is an array of free-form records, default []. The row-level calories, protein, carbs and fat columns hold the totals, and source is a MealSource:
The item records are written by the app and by the meal analysis. Do not assume a fixed shape. Read the totals from the columns and treat
items as display detail. The analysis output contract is in AI providers.
DailyMetric.waterLogs
An array validated by entrySchema in apps/core-api/src/modules/trainee/water-log.ts:
DailyMetric.waterMl is the running total. applyWaterLog makes writes idempotent: a repeated requestId is a no-op, an entry can be deducted only once, and the total can never go below zero.