Skip to content

Meal plan builder

Meal Plan Builder turns coach-authored meal structure into immutable nutrition plan snapshots clients can follow and log.

Tags: Nutrition, Builder drafts, Templates, Plan snapshots, Today handoff

Area: Nutrition / Coach Web / Meal Planning

The V1 baseline builder lets coaches create reusable nutrition templates, personalize them for a client, define day-type variants, add meal slots, foods, recipes, portions, approved alternatives, timing notes, target alignment, and publish a relationship-scoped plan version. Client mobile consumes the published snapshot through Today view and nutrition logging; clients never mutate the coach's builder draft.

Boundary decision

Meal Plan Builder owns draft authoring, template/version lifecycle, publish validation, plan snapshots, and assignment events. Macro and micronutrient tracking owns nutrient calculations, daily logs, aggregate variance, food facts, hydration, and supplement adherence. DailyExecution owns dated meal tasks and quick-log status.

Meal Builder Flows

Authoring is coach-owned, validation is explicit, and publishing creates stable snapshots for client execution.

Coach Starts A Draft

  1. Coach starts from a blank plan, reusable template, current client plan, archived version, or imported PDF draft.
  2. Coach selects plan mode: structured meals, macro targets plus meal suggestions, or hybrid plan.
  3. Draft setup stores title, description, owner, relationship target, locale, timezone, target set link, and effective date intent.
  4. Application service creates default day types such as training day and rest day, then inserts default meal slots from workspace preferences.
  5. Autosave writes expected builder revision and idempotency key so web and coach mobile drafts do not overwrite each other.

Coach Builds Meal Slots

  1. Coach chooses a day type, then creates or reorders meal slots such as breakfast, lunch, dinner, snack, pre-workout, or custom labels.
  2. Food search can add system seed foods, workspace foods, client-specific foods, coach manual macro entries, or recipe snapshots when the recipe library is enabled. External provider foods plug into the same selection flow only when a post-V1 provider is enabled.
  3. Each line stores serving unit, quantity, gram conversion snapshot, nutrient snapshot, notes, visibility, and source confidence.
  4. Coach can add meal-level or food-level alternatives with optional rules for equal macros, similar calories, preference-only, or coach-review-required.
  5. Draft totals update from snapshots and compare against the linked target set without treating catalog data as medical truth.

Validate And Publish

  1. Coach opens review to see blocking errors and warnings for empty required meals, missing serving units, target mismatch, low-confidence foods, and timing conflicts.
  2. Client profile restrictions are checked through read-only profile snapshots: allergies, dislikes, dietary pattern, cooking constraints, schedule limits, and medical flags.
  3. Publishing freezes the draft into a NutritionPlanVersion with meal slots, planned portions, alternatives, target-set reference, notes, windows, checksum, and version number.
  4. If the draft came from a reusable template, template metadata and latest version pointers are updated separately from relationship assignment.
  5. Publishing emits a nutrition plan event that refreshes future Today meal tasks, nutrition cache snapshots, and coach review summaries.

Client Follows And Logs

  1. Client mobile receives compact plan snapshots through Today and nutrition endpoints for the active local day and future prefetch window.
  2. Meal detail allows complete as planned, adjust portion, choose approved alternative, skip with reason, add unplanned food, or send a note.
  3. Accepted logs reference the plan version and planned food ids visible when the client logged the meal.
  4. Coach edits create a new draft and future plan version; completed historical logs continue to calculate from their original snapshots.

Meal Plan Builder UI

The coach-facing web builder should keep day type, meal slots, food search, and target variance visible at the same time.

Fat Loss Meal Plan

Client draft / revision 12

2,050 kcal target / training day

Training dayRest dayHigh carbTravel day

Breakfast07:00-09:00 / visible to client

510 kcal

  • Greek yogurt, 2 percent250 g
  • Mixed berries120 g
  • Whey isolate1 scoop

P 52gC 43gF 10gFiber 8g

Lunch12:00-14:00 / one approved swap

690 kcal

  • Chicken breast bowl recipe1 serving
  • Avocado60 g
  • Olive oil dressing12 g

P 58gC 71gF 19g

Dinner19:00-21:00 / needs review

580 kcal

  • Salmon fillet170 g
  • Jasmine rice150 g cooked
  • Green beans150 g

P 45gC 54gF 18g

Food Picker

Breakfast selected

Target gap: P +8g

Search foods, recipes, barcode...Verified

Workspace foodEgg whites

100 g / 52 kcal / P 11g / C 1g / F 0g

Recipe snapshotTurkey breakfast wrap

1 serving / 420 kcal / P 39g / C 38g / F 11g

System seed foodOats, rolled

40 g / 150 kcal / P 5g / C 27g / F 3g

Implementation note

Selecting a food writes a snapshot into the active meal slot. Later catalog edits can update search results but must not rewrite published plan versions.

Builder Rules

  • Day type tabs are explicit plan variants. Copying a meal or day type requires a server use case so target alignment and restrictions are recalculated.
  • Every meal slot has a stable slot_key used by Today tasks, client logs, and approved alternatives.
  • Drag-and-drop reorder changes only sort order; it never changes logged historical meal order.
  • Target totals show draft variance by day type and tracked nutrient, with warnings when a nutrient is not tracked for the client.

Coach Actions

  • Add meal: inserts a meal slot with label, time window, visibility, and optional instructions.
  • Add food: stores selected food or recipe snapshot, serving quantity, nutrient snapshot, and source confidence.
  • Duplicate: copies a meal slot, full day type, or template section into selected day types or relationships.
  • Review: opens validation grouped by blocking errors, warnings, client restrictions, target variance, and Today task impact.

Domain Model

Draft state is mutable and relational; published nutrition plan versions are immutable snapshots consumed by logging and Today tasks.

Aggregates & Entities

  • NutritionPlanTemplate: workspace-owned reusable identity with category, tags, goal labels, latest version pointer, and archive state.
  • NutritionPlanDraft: mutable builder aggregate for shell fields, day types, meal slots, food lines, alternatives, validation summary, and autosave revision.
  • NutritionPlanDraftDayType: variant such as training day, rest day, high-carb day, low-carb day, travel day, or custom key.
  • MealSlotDraft: ordered meal block with label, timing window, client instructions, visibility, and meal-level notes.
  • PlannedFoodDraftLine: selected food, recipe, or manual macro line with serving quantity, nutrient snapshot, source, and confidence.
  • MealAlternativeDraft: coach-approved replacement at meal or food-line level with optional policy and client-facing note.
  • NutritionPlanVersion: immutable published relationship plan with meal slots, planned portions, alternatives, target set reference, timing windows, and checksum.
  • NutritionPlanPublishReview: validation record that explains what was checked, accepted, warned, or blocked at publish time.

Value Objects & Rules

  • BuilderRevision: monotonically increasing integer required on every draft write to prevent lost autosaves.
  • DayTypeKey: stable key shared by target sets, meal slots, plan snapshots, and daily logs.
  • MealSlotKey: stable generated key that survives label changes and supports Today task idempotency.
  • ServingQuantity: quantity, serving unit id, gram equivalent when known, conversion confidence, and display label.
  • FoodSelectionSnapshot: food or recipe identity, display name, serving options, nutrient facts, allergens, dietary tags, source, and confidence.
  • TargetAlignment: per day type comparison against linked target values with variance status and tracked-nutrient visibility.
  • MealPlanValidationResult: blocking errors, warnings, profile conflicts, nutrient gaps, source confidence, and publish readiness.
  • Drafts can reference mutable catalog items, but published versions and accepted logs must store calculation snapshots.
  • Allergy and medical checks are safety warnings for coach review, not automated clinical decisions.
ModelOwned ByImportant MethodsEmits
NutritionPlanTemplateNutritioncreate(), startDraft(), recordVersion(), archive(), restore()NutritionPlanTemplateCreated, NutritionPlanTemplateArchived
NutritionPlanDraftNutritionsetSetup(), addDayType(), addMealSlot(), addFoodLine(), copyMealSlot(), validateForPublish(), publish()NutritionPlanDraftCreated, NutritionPlanDraftChanged, NutritionPlanPublished
MealSlotDraftNutritionrename(), setTimeWindow(), addFoodLine(), reorderFoodLines(), addAlternative(), snapshotForPublish()MealSlotDraftChanged
NutritionPlanVersionNutritionfromDraft(), assertAssignable(), resolveDayType(), snapshotForToday(), supersede()NutritionPlanPublished, NutritionPlanSuperseded
NutritionPlanPublishReviewNutritionrecord(), hasBlockingErrors(), explainWarnings(), toAudit()NutritionPlanValidationCompleted

Table Structure

Persist draft rows for builder operations, then publish into the immutable plan-version tables defined by nutrient tracking.

TablePurposeImportant ColumnsConstraints & Indexes
nutrition_plan_templatesReusable workspace-owned meal plan identity.id, workspace_id, owner_user_id, title, description, goal_tags text[], dietary_tags text[], status, latest_version_id, created_at, updated_at, archived_atIndex (workspace_id, status, updated_at desc); archive instead of delete when referenced by drafts or versions.
nutrition_plan_draftsMutable meal-plan builder state for templates, relationship plans, copies, or imports.id, workspace_id, template_id, source_plan_version_id, relationship_id, target_set_id, owner_user_id, source_type, plan_mode, status, title, timezone, builder_revision, validation_summary jsonb, last_saved_at, deleted_atIndex active drafts by (workspace_id, owner_user_id, status); relationship id is nullable for reusable templates.
nutrition_plan_draft_day_typesDay-type variants inside a draft.id, draft_id, day_type_key, label, target_set_day_type, sort_order, notes, created_at, updated_atUnique (draft_id, day_type_key); day type key must match target-set day type before publish when a target set is linked.
nutrition_plan_draft_meal_slotsMeal slots within a draft day type.id, draft_day_type_id, slot_key, label, sort_order, start_time, end_time, client_visible, instructions, notes, deleted_atUnique (draft_day_type_id, slot_key); index by (draft_day_type_id, sort_order); soft delete for undo.
nutrition_plan_draft_food_linesFood, recipe, or manual macro lines inside a draft meal slot.id, meal_slot_id, line_type, food_item_id, recipe_id, serving_unit_id, quantity, gram_weight_snapshot, selection_snapshot jsonb, nutrient_snapshot jsonb, source, confidence, sort_order, notes, deleted_atIndex (meal_slot_id, sort_order); require one of food, recipe, or manual macro payload based on line_type.
nutrition_plan_draft_alternativesCoach-approved swaps attached to a draft food line or meal slot.id, draft_id, source_food_line_id, meal_slot_id, line_type, food_item_id, recipe_id, serving_unit_id, quantity, selection_snapshot jsonb, nutrient_snapshot jsonb, swap_policy, client_note, sort_orderAt least one source reference is required; alternatives publish into meal_alternatives snapshot rows.
nutrition_plan_publish_reviewsStored validation result for publish attempts and audit.id, draft_id, workspace_id, relationship_id, target_set_id, status, blocking_errors jsonb, warnings jsonb, target_alignment jsonb, profile_conflicts jsonb, reviewed_by_user_id, created_atIndex latest by (draft_id, created_at desc); retain publish review with resulting plan version.
nutrition_plan_versionsImmutable published meal plan or hybrid plan consumed by Today and logging.id, workspace_id, relationship_id, template_id, source_draft_id, target_set_id, version, status, plan_mode, effective_from, effective_until, notes_snapshot jsonb, checksum, published_atUnique (relationship_id, version); published rows are immutable except lifecycle fields and supersession metadata.
meal_slot_templates, planned_food_portions, meal_alternativesPublished child rows from the draft tree.Use the snapshot columns defined by nutrient tracking: stable slot keys, food ids, serving units, quantities, gram snapshots, nutrient snapshots, notes, and sort order.Historical logs reference these published ids. Never rewrite rows used by accepted logs.

Implementation note

Coach mobile can mirror draft shell, day types, meal slots, and compact food lines for resume/edit. Client mobile mirrors only published active plan snapshots, approved alternatives, and enough food facts to render and log Today tasks offline.

Lifecycle Policy

Keep template authoring, client personalization, publication, and client logging separate.

Draft And Template Lifecycle

  • DRAFT: editable by authorized coaches and not visible to clients.
  • READY_FOR_REVIEW: no blocking validation errors, but warnings may remain.
  • PUBLISHED: freezes a relationship-scoped NutritionPlanVersion and publish review.
  • ARCHIVED: hidden from new template selection but retained for historical plan versions and audit.
  • Abandoned drafts can be soft-deleted after retention. Published plan versions stay readable while referenced by tasks, logs, analytics, or audit rows.

Assignment And Supersession

  • SCHEDULED: future effective date; Today materialization can prebuild upcoming meal tasks inside the configured window.
  • ACTIVE: plan snapshot used by Today, nutrition logging, client mobile cache, and coach review.
  • SUPERSEDED: future pending tasks move to the new version; completed logs keep their old plan reference.
  • CANCELED: stop future tasks but preserve the plan version for history.
  • Multiple active plans for one relationship are blocked unless the product later supports named concurrent plans.

Conflict Handling

  • Draft writes require expected builder_revision; stale writes return current revision and changed sections.
  • Food picker operations are idempotent by command id so a double tap does not add duplicate food lines.
  • Publish re-runs validation against the latest draft, target set, profile restrictions, and food snapshots.
  • If a coach edits today's plan while the client has queued offline meal logs, backend accepts logs against the plan version referenced by the client operation and returns a superseded-plan hint.

Snapshot Stability

  • Published portions and alternatives store nutrient snapshots, gram conversion, source, confidence, and display labels.
  • Accepted meal logs store their own calculation snapshots so later food database corrections do not rewrite history.
  • Template edits create a new draft or template version; they do not mutate assigned relationship plans.
  • PDF imports remain drafts until coach confirmation. Imported food guesses must show source confidence in review.

API Contracts

Coach endpoints manage draft authoring and publication. Client endpoints read published snapshots through nutrition and Today contracts.

EndpointPurposeRequestResponse
POST /api/v1/workspaces/{workspaceId}/nutrition/plan-draftsCreate a blank draft or copy from template, current plan, archived version, or import.Title, plan mode, source type, optional source id, optional relationship id, target set id, timezone.NutritionPlanDraftDto with default day types, meal slots, revision, and validation summary.
GET /api/v1/workspaces/{workspaceId}/nutrition/plan-drafts/{draftId}Load full builder state.Coach actor, locale, optional include catalog snapshots.Draft shell, day types, meal slots, food lines, alternatives, target alignment, validation summary, permissions.
PATCH /api/v1/workspaces/{workspaceId}/nutrition/plan-drafts/{draftId}/setupUpdate title, plan mode, linked target set, relationship, timezone, notes, or client visibility defaults.Expected revision and setup fields.Updated draft shell, target alignment refresh, next revision.
POST /api/v1/workspaces/{workspaceId}/nutrition/plan-drafts/{draftId}/day-typesAdd a day-type variant.Expected revision, day type key, label, target-set day type, copy-from day type option.Created day type, copied slots when requested, validation warnings, next revision.
POST /api/v1/workspaces/{workspaceId}/nutrition/plan-draft-day-types/{dayTypeId}/meal-slotsAdd a meal slot.Expected revision, label, start/end time, visibility, instructions, sort position.Created meal slot with stable slot key, target alignment, next revision.
PATCH /api/v1/workspaces/{workspaceId}/nutrition/plan-draft-meal-slots/{slotId}Rename, reorder, update timing, or change meal-level instructions.Expected revision, patch fields, optional ordered slot ids.Updated meal slot, validation summary, next revision.
POST /api/v1/workspaces/{workspaceId}/nutrition/plan-draft-meal-slots/{slotId}/food-linesAdd selected food, recipe, or manual macro line.Expected revision, line type, food item id or recipe id, serving unit, quantity, notes, idempotency key.Created food line with selection snapshot, nutrient snapshot, target alignment, next revision.
PATCH /api/v1/workspaces/{workspaceId}/nutrition/plan-draft-food-lines/{lineId}Adjust portion, serving unit, notes, visibility, or order.Expected revision and patch fields.Updated food line, recalculated meal/day totals, next revision.
POST /api/v1/workspaces/{workspaceId}/nutrition/plan-draft-alternativesAdd an approved meal or food-line alternative.Expected revision, source meal slot or source line id, selected food/recipe, serving quantity, swap policy, client note.Alternative row, nutrient comparison, validation warnings, next revision.
POST /api/v1/workspaces/{workspaceId}/nutrition/plan-drafts/{draftId}/copyCopy a meal slot, day type, or template section into selected day types or relationships.Expected revision, source reference, copy scope, target references, overwrite policy.Copied records, skipped targets with reasons, recalculated alignment, next revision.
POST /api/v1/workspaces/{workspaceId}/nutrition/plan-drafts/{draftId}/validateRun publish validation.Expected revision, validation mode, optional effective date.MealPlanValidationResultDto with blocking errors, warnings, target variance, profile conflicts, and anchors.
POST /api/v1/workspaces/{workspaceId}/nutrition/plan-drafts/{draftId}/publishPublish a relationship-scoped plan version.Expected revision, effective date range, template intent, notify client flag, publish note.NutritionPlanVersionDto, publish review, checksum, superseded version id, affected Today window.
GET /api/v1/coach/relationships/{relationshipId}/nutrition/plans/currentCoach reads active and scheduled plan versions for a relationship.Date, include history flag.Current plan summary, scheduled plan, latest validation summary, Today materialization status.
GET /api/v1/client/nutrition/today?date=YYYY-MM-DDClient reads active meal plan snapshot through the nutrient tracking contract.Local date, timezone, optional cache checksum.Targets, active plan version, meal slots, planned portions, alternatives, log state, and sync hints.

Key Code Snippets

Implementation sketches show the draft storage shape, food selection boundary, publish use case, and validation policy.

Drizzle schema shape

apps/api/src/nutrition/db/meal-plan-builder.schema.ts

ts
export const nutritionPlanDrafts = pgTable("nutrition_plan_drafts", {
  id: uuid("id").primaryKey().defaultRandom(),
  workspaceId: uuid("workspace_id").notNull(),
  templateId: uuid("template_id"),
  sourcePlanVersionId: uuid("source_plan_version_id"),
  relationshipId: uuid("relationship_id"),
  targetSetId: uuid("target_set_id"),
  ownerUserId: uuid("owner_user_id").notNull(),
  sourceType: text("source_type").$type<NutritionPlanDraftSource>().notNull(),
  planMode: text("plan_mode").$type<NutritionPlanMode>().notNull(),
  status: text("status").$type<NutritionPlanDraftStatus>().notNull().default("DRAFT"),
  title: text("title").notNull(),
  timezone: text("timezone").notNull(),
  builderRevision: integer("builder_revision").notNull().default(1),
  validationSummary: jsonb("validation_summary").$type<MealPlanValidationSummary>(),
  lastSavedAt: timestamp("last_saved_at", { withTimezone: true }).notNull().defaultNow(),
  deletedAt: timestamp("deleted_at", { withTimezone: true }),
}, (table) => ({
  ownerStatusIdx: index("nutrition_plan_drafts_owner_status_idx")
    .on(table.workspaceId, table.ownerUserId, table.status),
}));

export const nutritionPlanDraftFoodLines = pgTable("nutrition_plan_draft_food_lines", {
  id: uuid("id").primaryKey().defaultRandom(),
  mealSlotId: uuid("meal_slot_id").notNull(),
  lineType: text("line_type").$type<"FOOD" | "RECIPE" | "MANUAL_MACRO">().notNull(),
  foodItemId: uuid("food_item_id"),
  recipeId: uuid("recipe_id"),
  servingUnitId: uuid("serving_unit_id"),
  quantity: numeric("quantity", { precision: 12, scale: 4 }).notNull(),
  gramWeightSnapshot: numeric("gram_weight_snapshot", { precision: 12, scale: 4 }),
  selectionSnapshot: jsonb("selection_snapshot").$type<FoodSelectionSnapshot>().notNull(),
  nutrientSnapshot: jsonb("nutrient_snapshot").$type<NutrientSnapshot>().notNull(),
  source: text("source").$type<NutritionSource>().notNull(),
  confidence: text("confidence").$type<SourceConfidence>().notNull(),
  sortOrder: integer("sort_order").notNull(),
  deletedAt: timestamp("deleted_at", { withTimezone: true }),
}, (table) => ({
  mealSlotOrderIdx: index("nutrition_plan_draft_food_lines_slot_order_idx")
    .on(table.mealSlotId, table.sortOrder)
    .where(sql`${table.deletedAt} is null`),
}));

Add food line use case

apps/api/src/nutrition/use-cases/add-meal-plan-food-line.ts

ts
@Injectable()
export class AddMealPlanFoodLineUseCase {
  constructor(
    private readonly drafts: NutritionPlanDraftRepository,
    private readonly foodSelection: FoodSelectionSnapshotPort,
    private readonly validator: MealPlanDraftValidator,
    private readonly policy: NutritionPolicy,
    private readonly tx: TransactionRunner,
  ) {}

  async execute(command: AddFoodLineCommand, actor: Actor): Promise<NutritionPlanDraftDto> {
    return this.tx.run(async () => {
      const draft = await this.drafts.getForUpdate(command.draftId);
      this.policy.assertCoachCanEditMealPlanDraft(actor, draft);
      draft.assertRevision(command.expectedRevision);

      const snapshot = await this.foodSelection.createSnapshot({
        workspaceId: draft.workspaceId,
        lineType: command.lineType,
        foodItemId: command.foodItemId,
        recipeId: command.recipeId,
        servingUnitId: command.servingUnitId,
        quantity: command.quantity,
        locale: command.locale,
      }, actor);

      draft.addFoodLine(command.mealSlotId, snapshot, command.idempotencyKey);
      draft.recordValidation(this.validator.validateDraft(draft));

      await this.drafts.save(draft);
      return NutritionPlanDraftDto.fromDomain(draft);
    });
  }
}

Publish meal plan use case

apps/api/src/nutrition/use-cases/publish-meal-plan.ts

ts
@Injectable()
export class PublishMealPlanUseCase {
  constructor(
    private readonly drafts: NutritionPlanDraftRepository,
    private readonly plans: NutritionPlanVersionRepository,
    private readonly validator: MealPlanDraftValidator,
    private readonly todayInvalidation: TodayMaterializationPort,
    private readonly outbox: OutboxPort,
    private readonly tx: TransactionRunner,
  ) {}

  async execute(command: PublishMealPlanCommand, actor: Actor): Promise<NutritionPlanVersionDto> {
    return this.tx.run(async () => {
      const draft = await this.drafts.getForUpdate(command.draftId);
      draft.assertRevision(command.expectedRevision);
      draft.assertRelationshipScoped();

      const review = await this.validator.validateForPublish(draft, command.effectiveFrom, actor);
      if (review.hasBlockingErrors()) return NutritionPlanVersionDto.withReview(review);

      const plan = NutritionPlanVersion.fromDraft({
        draft,
        review,
        effectiveFrom: command.effectiveFrom,
        effectiveUntil: command.effectiveUntil,
        publishedByUserId: actor.userId,
      });

      await this.plans.supersedeFutureActive(plan);
      await this.plans.save(plan);
      await this.todayInvalidation.refreshNutritionWindow(plan.relationshipId, plan.effectiveRange());
      await this.outbox.publish(plan.pullDomainEvents());

      return NutritionPlanVersionDto.fromDomain(plan, review);
    });
  }
}

Validation policy

apps/api/src/nutrition/domain/meal-plan-draft-validator.ts

ts
export class MealPlanDraftValidator {
  validateForPublish(draft: NutritionPlanDraft, effectiveFrom: LocalDate, actor: Actor): MealPlanPublishReview {
    const review = MealPlanPublishReview.start(draft.id, effectiveFrom);

    review.require(draft.hasRelationship(), "RELATIONSHIP_REQUIRED");
    review.require(draft.hasAtLeastOneVisibleMeal(), "VISIBLE_MEAL_REQUIRED");
    review.requireAllMealSlotsHaveStableKeys(draft.mealSlots());
    review.requireFoodLinesHaveSnapshots(draft.foodLines());
    review.warnOnLowConfidenceFoods(draft.foodLines());
    review.warnOnTargetVariance(draft.targetAlignment());
    review.warnOnProfileConflicts(draft.profileRestrictionSnapshot());
    review.warnOnTimingConflicts(draft.nutritionWindows());

    return review.complete(actor.userId);
  }
}

Events & Background Jobs

Meal plan events refresh client execution snapshots while preserving the Nutrition boundary.

Events

  • NutritionPlanDraftCreated: updates draft list and autosave analytics.
  • NutritionPlanDraftChanged: invalidates draft summary, target alignment, and collaborator cache.
  • NutritionPlanValidationCompleted: stores publish review and can create coach task warnings for blocked plans.
  • NutritionPlanPublished: materializes meal tasks, refreshes client nutrition snapshots, and supersedes future pending tasks from the previous version.
  • NutritionPlanSuperseded: closes future task projections tied to the old version without touching completed logs.

Jobs And Projectors

  • Draft cleanup archives abandoned unpublished meal plan drafts after a retention window.
  • Template search projection indexes title, goal tags, dietary tags, meal count, target ranges, and last updated timestamp.
  • Target alignment projector recalculates draft totals after food line, serving, target set, or day type changes.
  • Today materializer creates or refreshes future meal tasks from published plan versions.
  • Client mobile snapshot generator emits compact plan payloads for offline prefetch and cache invalidation.

Guardrail

Do not emit clinical claims from validation. The builder can surface restrictions and nutrient gaps for coach review, but recommendations and plan changes remain coach-authored.

Permissions & Access Rules

Meal plans expose health preferences, allergies, and eating behavior, so every operation is workspace and relationship scoped.

Coach

  • Coach must have workspace membership and nutrition.plan.write permission to create, edit, publish, archive, or assign meal plans.
  • Coach can edit only drafts in their workspace and only relationship-scoped drafts for clients they are allowed to manage.
  • Publishing requires permission to read client nutrition profile restrictions and target sets for that relationship.
  • Coach cannot mutate published plan snapshots; editing starts a new draft from the existing version.
  • Shared workspace templates can be copied according to template visibility and ownership policy.

Client, Admin & System

  • Client can read only published plan snapshots active or historically visible for their own relationship.
  • Client cannot read builder drafts, template catalogs, coach-private notes, validation warnings hidden from clients, or source confidence metadata unless explicitly exposed.
  • Client meal logs can reference planned portions and approved alternatives, but cannot change the published plan itself.
  • System workers can materialize Today tasks, recalculate projections, and generate mobile sync snapshots with scoped service credentials.
  • Support/admin access to raw plans, profile restriction conflicts, imports, or deleted records requires explicit permission and audited reason.

Web, Coach Mobile, Client Mobile

Coach web carries the dense authoring experience; mobile surfaces should focus on review, small edits, and execution.

Coach Web

  • Full builder with day-type tabs, meal slot editing, food/recipe picker, target variance, validation, copy tools, and publish review.
  • Can create reusable templates, personalize for a relationship, assign future effective dates, and inspect Today task impact before publish.
  • Shows source confidence, allergy/restriction warnings, hidden coach notes, and target alignment not necessarily visible to clients.
  • Supports PDF import review as draft data, never as auto-published plan content.

Coach Mobile

  • Can review active plan summaries, duplicate simple templates, adjust small portions, and publish only when validation is clean.
  • Complex drag-and-drop builder operations can deep link to web or use simplified stepper flows.
  • Offline drafts store shell, selected day type, meal slots, and pending edits with builder revision conflict handling.
  • Coach can respond to meal swap requests by messaging or starting a new draft from the active version.

Client Mobile

  • Today renders active meal tasks, timing hints, planned foods, quantities, approved alternatives, and coach instructions from SQLite cache first.
  • Meal detail supports complete as planned, portion adjustment, approved swap, skip reason, unplanned food, and note.
  • Visible totals update locally from cached snapshots and reconcile with canonical backend totals after sync.
  • Client views respect Arabic/English, RTL, dynamic type, dark mode, long food names, and accessible macro status labels.

Test Scenarios & V1 Baseline Decisions

Builder tests need strong versioning, validation, permission, and mobile handoff coverage.

Tests

  • Domain unit tests: day type creation, slot key stability, food line snapshots, alternative policy, copy operations, target alignment, and validation severity.
  • Versioning tests: draft publish freezes snapshots, template edits do not mutate assigned plans, superseded plans keep historical logs readable.
  • API integration tests: create draft, add meal slot, add food, copy day type, validate, publish, supersede previous plan, and refresh Today tasks.
  • Conflict tests: stale builder revision rejection, duplicate idempotency key, simultaneous food-line edit, and publish after target set change.
  • Permission tests: coach cannot edit another workspace draft, client cannot read drafts, support raw import access is audited.
  • Food and recipe tests: missing gram conversion, archived food, low-confidence micronutrients, recipe snapshot publish, and source refresh without history rewrite.
  • Mobile E2E tests: client opens Today offline, sees active meal plan snapshot, completes a planned meal, reconnects, and coach review shows accepted plan version.
  • Accessibility and localization tests: long food labels, Arabic/English labels, RTL day-type tabs, screen-reader macro summaries, dynamic type, and dark mode contrast.

Resolved V1 Baseline Decisions

  • Meal builder ships without waiting for Recipe Library. Recipe Library is a parallel Nutrition subfeature; when available, it supplies immutable recipe snapshots consumed by the builder the same way food snapshots are consumed.
  • New workspace or coach drafts default to one EVERYDAY day type with Breakfast, Lunch, Dinner, and Snack slots and no fixed times. Training Day, Rest Day, and custom day types are optional additions copied from templates or target-set variants.
  • Clients see meal instructions, planned foods, portions, timing hints, and approved alternatives by default. Calories, macros, target bands, and remaining/over hints are visible only for target values the coach marks client-visible.
  • Publish validation blocks confirmed allergy and medical-restriction conflicts. Preference conflicts, low-confidence restriction data, and non-allergy dietary mismatches are warnings that require coach acknowledgement and a publish note.
  • Future materialization and client mobile prefetch cover today plus the next 7 local days, refreshed on publish, supersede, timezone change, or snapshot invalidation.
  • Macro-only prescriptions stay in target-set setup for Public V1. They may create a lightweight macro-only plan version for DailyExecution materialization, but they do not use the full meal-builder shell unless the coach adds meals or hybrid structure.

CoachMe internal planning documentation.