Appearance
Domain model map
One visual reference for contexts, aggregates, entities, and value objects.
Tags: Bounded Contexts, Aggregates, Entities, Value Objects
Area: CoachMe DDD relationship map
This page is the source-of-truth graph for bounded-context ownership, aggregate roots, and cross-context references. As new feature pages are designed, we can add focused domain-map pages and link them back here.
Bounded contexts
The ten contexts below match docs/doc-map.md section 2 and the apps/api/src/<context> layout. Each can later get its own graph page when its aggregates become too dense.
Status here describes documentation depth, not build state — feature code is still at zero per the development readiness review. "Specced" means the context has feature technical pages that define its aggregates.
IdentityAccess
Specced. Owns users, auth identities, app sessions, provider availability, provider policy, and provider audit history. Its identity and membership aggregates sit outside the tenant fence — see Tenant scope.
CoachClientManagement
Specced. Owns client profiles, relationship-scoped requirements, readiness reviews, notes, onboarding workflows, measurement plans, progress entries, and photo evidence.
Forms
Specced. Owns questionnaire templates, versions, assignments, schedules, submissions, and review tasks. See questionnaire-builder, media-form-attachments.
ProgramsAndExercise
Specced. Owns the exercise catalog and taxonomy, program templates, drafts, published versions, assigned programs, and program days. See program-builder, exercise-library.
DailyExecution
Specced. Owns Today items, quick logs, workout execution state, session logs, set-level RPE/RIR, the offline submission outbox, and adherence signals. See today-view, workout-logging, offline-workout-mode.
Nutrition
Specced. Owns the nutrient and food catalog, nutrition plan templates, drafts and published versions, relationship targets, daily intake logs, meal swap requests, and supplement schedules. Part of the Core Coaching + Nutrition Baseline for Public V1. See the four nutrition-* feature pages.
Communication
Specced. Owns conversation threads, messages, voice-note assets, notification intents and deliveries, and push installations. See in-app-messaging, voice-notes, push-notifications.
ProgressAnalytics
Specced. Consumes events and builds read models for timelines, charts, PRs, effort trends, comparisons, adherence scoring, risk flags, and weekly recaps. Owns no write model of its own. See client-risk-flags, priority-view, coach-task-inbox.
Integrations
Specced. Owns provider adapters and their configuration, credential references, and sync policy. Contexts consume providers through this boundary rather than calling them directly. See ymove-exercise-integration and the integration register.
Billing
Post-V1. No aggregates defined yet; listed so the map stays complete against doc-map.
Aggregate and entity relations
These relation statements are intentionally explicit so they can become tests, database constraints, and API authorization rules later.
User→ owns manyAuthIdentityrecords; uniqueness is scoped by provider, issuer, tenant, and provider subject.User→ owns oneCoachProfilewhen the user has the coach role and completed registration.CoachWorkspace→ containsWorkspaceMembershiprecords; the first coach registration creates anOWNERmembership.ClientProfile→ owns profile sections for goals, health, nutrition, training, preferences, and locale/timezone baseline.ProfileRequirementSet→ belongs to a coach-client relationship and decides which profile sections must be complete before review.ClientProfileReview→ records relationship-specific readiness; profile completion does not equal coach approval.OnboardingWorkflowDefinition→ stores the system-defined V1 baseline flow used by client invitations and relationship-scoped workflow instances.OnboardingWorkflowInstance→ contains orderedOnboardingTaskrecords and references, but does not own, form submissions, profile sections, measurements, or photos.OnboardingOverride→ records coach/system cancellation, reassignment, nudges, or force-complete decisions as append-only audit facts.FormTemplate→ owns draft form metadata, whileFormVersionfreezes the published question definition.FormAssignment→ targets one relationship and one published form version;FormSchedulegenerates repeated assignments.FormSubmission→ stores answers for a specific assignment/version and can apply explicitFormFieldMappingrules.MeasurementPlan→ owns requiredMeasurementPlanItemrules and one active plan exists per relationship.MeasurementEntry→ contains manyMeasurementValuevalues and may link to a check-in/task submission.ProgressPhotoSet→ contains angle-specificProgressPhotoAssetrecords stored through the shared object-storage layer.WorkoutSession→ contains performed exercise and set logs, including submitted load, reps, notes, and RPE/RIR readings.PersonalRecordEvent→ is projected from submitted workout logs and feedsCurrentPersonalRecordread models.ProgramTemplate→ owns reusable program identity;ProgramDraftis the mutable builder andProgramVersionthe immutable published snapshot assigned to relationships.AssignedProgram→ references one publishedProgramVersionby id and contains generatedProgramDayrecords; editing a template never mutates an active assignment.ExerciseCatalogItem→ spans the globalfree-exercise-dbimport and coach-authored custom exercises; the imported rows are un-fenced, the custom rows are tenant-scoped.NutritionPlanTemplate→ owns reusable plan identity, mirroring the template/draft/version split used by programs.NutritionPlanVersion→ freezes meal slots, planned portions, alternatives, and supplement links;DailyNutritionLogreferences it by id rather than embedding it.NutritionTargetSet→ owns relationship-scoped targets and day-type mappings; exactly one set is effective per relationship at a time.MealSwapRequest→ records a client request against a specific plan version and resolves through an explicit coach decision; approval may or may not update the plan.ConversationThread→ is relationship-scoped and containsConversationMessagerecords; voice notes are a message kind, not a separate timeline.NotificationIntent→ is a provider-neutral request to notify one recipient about one source event;NotificationDeliveryrecords one attempt through one provider to onePushInstallation.ExerciseProviderIntegration→ owns provider configuration and credential references; contexts reach providers through this boundary rather than calling them directly.
Tenant scope
multi-tenancy owns the tenant fence and defines four table classes; it delegates aggregate ownership to this page, and its foreign-key rule follows the ownership verbs above. This section is the join between the two: it declares which class each aggregate root falls in, using that page's vocabulary unchanged.
Tenant-scoped is the default. An aggregate is listed below only if it is not tenant-scoped, or if it needs an argument for why it is.
| Aggregate root | Class | Why |
|---|---|---|
User, AuthIdentity, CoachWorkspace, WorkspaceMembership | Identity and membership | Read before a workspace is known, to answer which workspaces the user may select. Fencing them by workspace would make context selection impossible. |
ExerciseCatalogItem, ExerciseTaxonomyNode | Global reference | The approved free-exercise-db import and its taxonomy, un-fenced and readable by all tenants. Coach-authored custom exercises are tenant-scoped and live alongside it. |
OnboardingWorkflowDefinition | Global reference | The system-defined V1 baseline flow, written by migration and read by every workspace. Its OnboardingWorkflowInstance records are tenant-scoped. |
OnboardingOverride | System and operational | Append-only audit facts, reached only under an explicit support context. |
NotificationIntent, NotificationDelivery, PushReceipt | System and operational | Carry workspace_id so workers can route and scope, but are never reached ambiently from a request. |
Everything else on this page is tenant-scoped: client profiles, relationships, programs, assignments, logs, forms, messages, media metadata, notes, and tasks all carry workspace_id and are reachable only through a tenant-scoped repository.
Open scope questions
These aggregates do not fall cleanly into one class. Per docs/doc-map.md, they stay open here rather than being resolved to match an assumption.
FoodItem,NutrientDefinition, andSupplementDefinitioneach describe both a system-seeded catalog row and a workspace- or coach-owned row. That spans global reference and tenant-scoped in one aggregate. Either it splits into two tables, or the seeded rows carry a nullableworkspace_idand lose the structural guarantee. multi-tenancy requires every table to declare its class in review; these three cannot until this is decided.AuthProviderConfigis provider policy read at login, before a workspace is resolved. It is probably global reference, but it is written by admins rather than by migration, which does not match that class's write rule.PushInstallationbinds a user and an app surface, not a relationship. Whether it is identity and membership or tenant-scoped depends on whether a user with two workspaces needs separate installations.
Value objects and invariants
Value objects are grouped by the aggregate or boundary where they enforce rules. This section should grow with each new feature page.
Identity value objects
ProviderRef: provider, issuer, tenant, subject, verification metadata.EmailAddressandPhoneNumber: normalized before uniqueness checks.TermsAcceptance: immutable accepted version, timestamp, IP hash, and user agent.
Provider policy objects
ProviderKey: stable internal provider key.ProductSurface:WEB,COACH_MOBILE,CLIENT_MOBILE.VerificationPolicy: verified email, phone, or either.
Profile value objects
ProfileSectionKey: personal, goals, health, nutrition, training, preferences.FieldRequirement: requirement rule, source, visibility, validation type.ProfileReadinessStatus: review and programming state.
Onboarding value objects
OnboardingTaskType: V1 baseline uses questionnaire; future types can add profile, consent, resource, measurement, and photo tasks.OnboardingTaskStatus: ready, opened, skipped for now, completed, cancelled, expired.WorkflowStatus: in progress, completed, completed with pending task, paused, cancelled.
Progress value objects
MeasurementKind: body weight, circumference, body composition, performance, custom.MeasurementUnit: submitted and canonical units.PhotoAngle: front, side, back, custom.
Progress invariants
- Submitted entries are corrected by versioning, not edited in place after coach review.
- Photo assets are hidden until scan, metadata stripping, thumbnail generation, and permissions pass.
- Progress date and upload timestamp are separate business facts.
Workout execution values
EffortMetric: none, RPE, or RIR.EffortReading: validated effort value captured from client logs.PerformedLoad: submitted unit plus canonical load for comparison.
Performance values
PersonalRecordType: max load, estimated 1RM, reps at load, volume.ExerciseRecordScope: relationship plus performed exercise identity.EffortTrendWindow: weekly or program-block rollup period.
Cross-context references
- Use IDs and events across contexts, not object ownership.
- Downstream contexts consume facts from profiles and progress evidence.
- Relationship access is enforced in use cases and API guards.
Relationship graph
The graphs show ownership first, then the most important cross-context references and event flows. Arrows point from owner or producer to dependent model or consumer. The Mermaid source is canonical; edit it directly in this page.
Read the context graph for "what talks to what", then the per-context graphs for "what owns what" inside one boundary.
Legend
Node kind is carried by shape, so labels stay short enough to read:
| Shape | Mermaid | Kind |
|---|---|---|
| Double-bordered box | [[X]] | Bounded context |
| Rectangle | [X] | Aggregate root — consistency and transaction boundary |
| Rounded box | ([X]) | Entity owned by an aggregate root, no independent lifecycle |
| Cylinder | [(X)] | Read model — a projection, never written to directly |
| Hexagon | | Value object or enum that enforces a rule |
Edge kind is carried by line style:
- Solid arrow — ownership or containment, inside one aggregate or context.
- Dashed arrow — a reference by id, an emitted event, or a read-model feed.
Keep the diagrams legible
Mermaid scales each diagram down to fit the prose column, so a wide graph becomes unreadable text rather than a scrollbar. Two rules keep that from happening: use flowchart TD (top-down grows down the page, not across it), and never put the node kind in the label — that is what the shapes are for. A graph past roughly 12 nodes should be split, not shrunk.
Context graph
The cross-context view: what talks to what. Solid arrows are the flow of work through the product; dashed arrows are event or read-model feeds. No aggregate detail here — that is the next section.
Billing is drawn unconnected on purpose: it is Post-V1 and its relationships are not yet decided. An edge here would assert a contract that no feature page defines.
Per-context aggregate graphs
One graph per boundary, each small enough to read. Cross-context references are deliberately absent here — they live in the context graph and in Cross-aggregate references below — so that each graph answers exactly one question: what does this context own, and how does it hang together?
When a context outgrows its graph, promote it to a child page and link it from here.
IdentityAccess
CoachClientManagement — client profiles
Requirements, reviews, and notes all reference the profile rather than nest inside it: a profile is one client's data, while what is required and whether it is approved are facts about a coach-client relationship.
CoachClientManagement — onboarding
CoachClientManagement — progress evidence
Forms
ProgramsAndExercise
Nutrition — plan authoring
Nutrition mirrors the template/draft/version split used by programs: a mutable draft, then an immutable published version that assignments reference.
Nutrition — client execution
NutritionPlanVersion is the hub: logs, swaps, and supplement rows all reference the published prescription rather than copy it.
DailyExecution and ProgressAnalytics
ProgressAnalytics owns no write model. Everything it holds is a projection, which is why its nodes are cylinders and its only inbound edges are builds read model from.
Communication and Integrations
Cross-aggregate references
These are the edges that cross an aggregate or context boundary. They are listed rather than drawn, because drawing them is what turned this page into an unreadable schema diagram. Each one is a plain id column with no foreign key, resolved by a tenant-scoped read — see Prefer language precision.
| Holder | References by id | Why it crosses |
|---|---|---|
ProfileRequirementSet | FormVersion | Requirements decide which published form satisfies a section. |
OnboardingTask | FormAssignment | A task points at the assignment it opens; it does not own the submission. |
FormSubmission | ClientProfile | FormFieldMapping writes answers into profile sections through an event, not a join. |
ExerciseCatalogItem | ExerciseProviderIntegration | Catalog rows record which provider supplied them. |
WorkoutSession | AssignedProgram | Execution references the assigned snapshot it was performed against. |
NotificationIntent | any source aggregate | Intents name a source event, staying provider- and context-neutral. |
Domain events
Every dashed emits edge above is carried by a published event, but this page does not name events. event-and-outbox-standard owns event naming, the envelope, versioning, and outbox behavior; duplicating names here would create a second source of truth that drifts.
What this page contributes is the shape: an event is emitted by the aggregate root that owns the fact, and consumed by another context that must not reach into that aggregate. Read an emits edge as "the owner publishes a fact; the consumer subscribes" — never as a synchronous call.
Names follow context.aggregate.fact.v1, as in client.relationship.activated.v1 or nutrition.meal_swap.approved.v1. When a feature page defines a new event, add the edge here and the name there.
How to use this source
- Keep names aligned with feature technical pages.
- Add each new feature's aggregate root as soon as the feature page gets a Domain Model section.
- Put cross-context flow in the context graph and aggregate internals in that context's own graph. Never add aggregates to the context graph.
- Carry node kind in the shape, never in the label. A label is a name and nothing else.
- Use
flowchart TD. Left-to-right graphs grow wider than the prose column and get scaled into illegibility. - List a boundary-crossing reference in Cross-aggregate references rather than drawing it into a per-context graph.
- Use solid arrows for ownership and dashed arrows for events, read-model feeds, or references by id.
- Declare the tenant class of every new aggregate root in Tenant scope, or record why it is undecided.
- If a context passes roughly 12 nodes, split it — a second graph here, or a child page.
Growth rules for future pages
As CoachMe grows, this page remains the overview and individual contexts get deeper graph pages.
One overview map
Keep this page focused on cross-domain understanding. It should answer, “what owns what, and what talks to what?” without becoming a full schema diagram.
Many focused child pages
Create pages such as /features/domain-identity-access, /features/domain-client-management, and /features/domain-programs-exercise when a context needs detailed aggregate internals.
Every feature contributes
When a feature technical page defines aggregates, entities, value objects, events, repositories, or external ports, add the changed nodes and edges here.
Prefer language precision
The aggregate relation graph uses a closed set of verbs, because each one has a consequence in code:
| Verb | Means | Consequence |
|---|---|---|
owns | A bounded context is the write authority for this aggregate root. | Only this context's use cases may mutate it. |
contains | An entity or value object inside an aggregate boundary. | Same transaction, same repository, cascades with the root. A foreign key is allowed here. |
references by id | A pointer to an aggregate the holder does not own. | Store a plain id. No navigation, no cascade, and per multi-tenancy no foreign key across an aggregate or context boundary — the reference is resolved by a tenant-scoped read and validated as an invariant. |
emits | The owner publishes a fact after commit. | Goes through the outbox; consumers are async and idempotent. |
consumes | A context subscribes to another's fact. | Never a synchronous call into the producer. |
builds read model from | A projection derived from events. | Rebuildable, never written to directly. |
If an edge needs a verb outside this set, that usually means the relationship is not yet understood — say it in prose in Aggregate and entity relations first, and add the edge once it resolves to one of the six.
The context graph is the exception: it is a narrative overview, so descriptive edge labels like "supplies assigned snapshots to" are fine there. Precision is enforced where it has consequences.