Appearance
Client Management / Forms / Identity
Onboarding workflow is a fixed V1 baseline sequence: register from invite, then complete or skip the coach questionnaire task.
The onboarding workflow coordinates the handoff from validated invitation and client registration into a predictable client mobile checklist. In V1 baseline, every invited client gets the same system-defined flow: create/login account, receive the coach questionnaire task, optionally skip it during first-run onboarding, and complete it later from the client task list.
Page status
- Fixed V1 baseline flow
- Relationship-scoped instance
- Skippable questionnaire task
- Resume later
V1 baseline decisions
V1 baseline decisions: do not build coach-configurable onboarding templates yet. Questionnaire skippability is fixed for V1 baseline and cannot be made non-skippable for specific clients. After skip, route the client to a lightweight welcome screen. Skipped questionnaires trigger daily reminders and block program and nutrition assignment until submitted or cancelled. Cancelled questionnaire assignments and onboarding task history are retained for 30 days after relationship termination.
Onboarding Workflow Flows
These flows keep auth, invitation validation, and form submission details in their owning technical pages while defining the workflow orchestration contract.
Coach Prepares Invitation
- Coach creates or selects the onboarding questionnaire using the form builder.
- Coach sends a client invitation from client management.
- Invitation stores the selected questionnaire/form version or falls back to the workspace default onboarding questionnaire.
- System associates the invitation with the system-defined V1 baseline onboarding workflow version.
- No coach-authored workflow template is needed for V1 baseline.
Client Registers From Invite
- Client opens a valid invitation link and sees safe preview copy.
- Client completes provider login/registration through client onboarding registration.
- Registration transaction creates or loads the client user, client profile, coach-client relationship, onboarding instance, and questionnaire task.
- API returns session context with
nextRoute=ONBOARDING_QUESTIONNAIREwhen the task is ready. - Client mobile shows the questionnaire as the first task after account creation.
Client Completes Or Skips Questionnaire
- Client opens the questionnaire task and submits answers through the Forms API.
- Successful form submission marks the onboarding task
COMPLETEDand routes the client to the next available app surface. - Client may choose
Skip for now; the task becomesSKIPPED_FOR_NOW, the workflow becomesCOMPLETED_WITH_PENDING_TASK, and the relationship can continue. - After skip, client lands on a lightweight welcome screen instead of Today or profile completion.
- Skipped questionnaire remains visible in the client task list/Today view until submitted or cancelled by coach policy, and the system sends daily reminders.
- Coach sees questionnaire status from client profile context and can nudge, message, or manually cancel/reassign. Program and nutrition assignment remain blocked while the skipped questionnaire is pending.
Returning Later
- Client opens the app after registration and calls
GET /api/v1/client/onboarding. - API returns completed onboarding state plus pending skipped questionnaire task when applicable.
- Client can resume the questionnaire without revalidating the invitation.
- Submitted answers are stored as a form submission and can map into profile fields according to explicit mapping rules.
- If relationship is paused or ended, the task becomes read-only or hidden according to relationship policy.
Note
Skipping the questionnaire skips only the first-run onboarding gate; it does not delete the form assignment, mark the questionnaire as answered, or unlock program/nutrition assignment.
Domain Model
Onboarding owns orchestration state. Forms own questionnaire versions, assignments, submissions, and reviews. Client profiles own current baseline fields derived from explicit mappings.
Aggregates & Entities
OnboardingWorkflowDefinition: system-defined, versioned flow definition. V1 baseline has one active definition for invited clients.OnboardingWorkflowInstance: relationship-scoped execution state created by client onboarding registration.OnboardingTask: concrete task generated for the relationship. V1 baseline task is the coach questionnaire assignment.OnboardingTaskEvent: append-only task history for created, opened, skipped, resumed, completed, expired, or cancelled transitions.OnboardingOverride: coach/system override such as cancel, reassign questionnaire, request completion, or force complete for support.FormAssignment: Forms aggregate referenced from the onboarding task payload.
Value Objects & Rules
OnboardingTaskType: V1 baseline usesQUESTIONNAIRE; future values can include profile section, consent, resource review, measurement, photo, or coach review.OnboardingTaskStatus:READY,OPENED,COMPLETED,SKIPPED_FOR_NOW,CANCELLED,EXPIRED.WorkflowStatus:IN_PROGRESS,COMPLETED,COMPLETED_WITH_PENDING_TASK,PAUSED,CANCELLED.SkipPolicy: questionnaire skip is fixed for V1 baseline. Coaches cannot make it non-skippable for specific clients; client can skip during first-run onboarding, but the task remains active until submitted or cancelled.ResumePolicy: skipped questionnaire can be resumed from task list without invitation token.ReminderPolicy: skipped questionnaire tasks create daily reminders until submitted, cancelled, relationship ended, or notification policy suppresses delivery.AssignmentGate: skipped questionnaire blocks program and nutrition assignment. Completion or audited cancellation clears the block.- Task completion is idempotent by task id plus form assignment/submission id.
| Model | Owned By | Important Methods | Emits |
|---|---|---|---|
OnboardingWorkflowDefinition | CoachClientManagement | activeForInvite(), generateTasks() | OnboardingWorkflowDefined |
OnboardingWorkflowInstance | CoachClientManagement | startFromRegistration(), complete(), completeWithPendingTask(), pause(), cancel() | ClientOnboardingStarted, ClientOnboardingFinished |
OnboardingTask | CoachClientManagement | open(), completeFromSubmission(), skipForNow(), resume(), cancel() | OnboardingTaskSkipped, OnboardingTaskCompleted |
FormAssignment | Forms | Created from selected onboarding questionnaire version and referenced by task payload. | FormAssignmentCreated, FormSubmissionReceived |
ClientProfile | CoachClientManagement | Receives explicit mapped values after form submission mapping succeeds. | ClientProfileUpdatedFromForm |
Table Structure
Keep V1 baseline onboarding tables small and relationship-focused. Store versioned workflow definition for auditability, but do not expose coach-configurable template editing yet.
| Table | Purpose | Important Columns | Constraints & Indexes |
|---|---|---|---|
onboarding_workflow_definitions | System-defined versioned flow definition. | id, key, version_number, status, definition jsonb, activated_at, retired_at | Unique active key; immutable definition after activation; seed with CLIENT_INVITE_V1 baseline. |
onboarding_workflow_instances | Relationship-scoped onboarding execution. | id, relationship_id, definition_id, invitation_id, status, program_assignment_blocked, nutrition_assignment_blocked, started_at, finished_at, current_task_id, created_at, updated_at, retention_expires_at | Unique (relationship_id, definition_id); index (relationship_id, status); do not hard-delete during normal use; set retention_expires_at to relationship termination plus 30 days. |
onboarding_tasks | Concrete task generated for the workflow instance. | id, workflow_instance_id, task_key, task_type, status, sort_order, form_assignment_id, due_at, opened_at, skipped_at, next_reminder_at, reminder_cadence, completed_at, cancelled_at, payload jsonb, retention_expires_at | Unique (workflow_instance_id, task_key); index (form_assignment_id); index (status, next_reminder_at) for daily skipped-task reminders. |
onboarding_task_events | Append-only state transition history. | id, task_id, event_type, actor_user_id, metadata jsonb, created_at, retention_expires_at | Index (task_id, created_at); used for audit and support timelines; retention cleanup anonymizes or purges after relationship termination plus 30 days. |
onboarding_overrides | Coach/system override audit. | id, task_id, actor_user_id, override_type, reason, created_at | Append-only; reason required for cancel, force-complete, or reassignment. |
form_assignments | Questionnaire task target owned by Forms. | id, form_version_id, relationship_id, source_type, source_id, status, submitted_at, cancelled_at, retention_expires_at | source_type=ONBOARDING and source_id=onboarding_task.id; status drives task completion; cancelled onboarding assignments follow the 30-day post-termination retention window. |
outbox_events | Transactional handoff to Forms, notifications, coach inbox, and profile mapping jobs. | id, aggregate_type, aggregate_id, event_type, payload jsonb, status, available_at | Standard outbox worker with retries; payload references IDs only. |
Lifecycle Policy
Onboarding history should explain whether the client answered the questionnaire, skipped it, or was allowed to continue with it pending.
Workflow Lifecycle
- Workflow instance starts after the invitation registration transaction commits.
IN_PROGRESSwhile first-run questionnaire task is visible and not completed/skipped.COMPLETEDwhen questionnaire submission is received during onboarding.COMPLETED_WITH_PENDING_TASKwhen the client skips questionnaire and enters the lightweight welcome screen with the task still active.- Skipped questionnaire keeps
program_assignment_blockedandnutrition_assignment_blockedtrue until submitted or cancelled. PAUSEDorCANCELLEDfollows relationship status changes.
Questionnaire Task Lifecycle
READYafter task/form assignment creation.OPENEDwhen client starts the form.SKIPPED_FOR_NOWwhen client bypasses first-run onboarding; setreminder_cadence=DAILYandnext_reminder_at.COMPLETEDonly after a successful form submission is linked.CANCELLEDonly by coach/system override, preserving event history and clearing program/nutrition blocks.
Retention
Retention: cancelled questionnaire assignments and onboarding task history remain read-only for 30 days after relationship termination, then are purged or anonymized from normal access. Questionnaire answers follow the Forms retention/deletion policy because submissions can include sensitive health and nutrition data.
API Contracts
Client endpoints drive the first-run and resume flows. Coach endpoints are limited to status visibility and override/reassignment actions in Public V1.
| Endpoint | Purpose | Request | Response |
|---|---|---|---|
GET /api/v1/client/onboarding | Return current onboarding state after registration or app restart. | Authenticated client session; optional relationshipId. | Workflow status, questionnaire task, form assignment id, fixed skip eligibility, assignment blocks, and next route. |
POST /api/v1/client/onboarding/tasks/{taskId}/open | Mark task opened and return launch context. | Client timestamp and idempotency key. | Updated task plus formAssignmentId and form launch route. |
POST /api/v1/client/onboarding/tasks/{taskId}/skip | Skip questionnaire during first-run onboarding. | Optional client reason, idempotency key. | Task SKIPPED_FOR_NOW, workflow COMPLETED_WITH_PENDING_TASK, nextRoute=LIGHTWEIGHT_WELCOME, daily reminder schedule, and program/nutrition assignment blocks. |
POST /api/v1/client/onboarding/tasks/{taskId}/complete-from-form | Complete task after Forms confirms submission. | formSubmissionId, idempotency key. | Task COMPLETED, workflow status, assignment blocks cleared, next route. |
GET /api/v1/workspaces/{workspaceId}/clients/{clientId}/onboarding | Coach reads onboarding status from client profile context. | Authorized workspace coach. | Workflow summary, questionnaire status, submitted/skipped timestamps, actions. |
POST /api/v1/workspaces/{workspaceId}/clients/{clientId}/onboarding/tasks/{taskId}/override | Coach cancels, reassigns, nudges, or force-completes with audit. | Override type, reason, optional replacement form version. | Updated task, override record, side-effect summary. |
Key Code Snippets
Implementation sketches for the planned TypeScript/NestJS/Drizzle stack. Keep onboarding orchestration in the CoachClientManagement application/domain layer and questionnaire answers in Forms.
Drizzle schema shape
apps/api/src/coach-client-management/db/onboarding.schema.ts
ts
export const onboardingWorkflowInstances = pgTable("onboarding_workflow_instances", {
id: uuid("id").primaryKey().defaultRandom(),
// External aggregate references. Do not import/navigate relationship, invitation, or form rows here.
relationshipId: uuid("relationship_id").notNull(),
invitationId: uuid("invitation_id").notNull(),
definitionKey: text("definition_key").notNull().default("CLIENT_INVITE_V1 baseline"),
definitionVersion: integer("definition_version").notNull(),
status: text("status").$type<WorkflowStatus>().notNull().default("IN_PROGRESS"),
programAssignmentBlocked: boolean("program_assignment_blocked").notNull().default(false),
nutritionAssignmentBlocked: boolean("nutrition_assignment_blocked").notNull().default(false),
currentTaskId: uuid("current_task_id"),
startedAt: timestamp("started_at", { withTimezone: true }).notNull().defaultNow(),
finishedAt: timestamp("finished_at", { withTimezone: true }),
retentionExpiresAt: timestamp("retention_expires_at", { withTimezone: true }),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
}, (table) => ({
relationshipDefinitionUnique: uniqueIndex("onboarding_instances_relationship_definition_uq")
.on(table.relationshipId, table.definitionKey, table.definitionVersion),
relationshipStatusIdx: index("onboarding_instances_relationship_status_idx")
.on(table.relationshipId, table.status),
}));
export const onboardingTasks = pgTable("onboarding_tasks", {
id: uuid("id").primaryKey().defaultRandom(),
workflowInstanceId: uuid("workflow_instance_id").notNull()
.references(() => onboardingWorkflowInstances.id),
taskKey: text("task_key").notNull(),
taskType: text("task_type").$type<"QUESTIONNAIRE">().notNull(),
status: text("status").$type<OnboardingTaskStatus>().notNull().default("READY"),
formAssignmentId: uuid("form_assignment_id"),
sortOrder: integer("sort_order").notNull().default(0),
openedAt: timestamp("opened_at", { withTimezone: true }),
skippedAt: timestamp("skipped_at", { withTimezone: true }),
nextReminderAt: timestamp("next_reminder_at", { withTimezone: true }),
reminderCadence: text("reminder_cadence").$type<"DAILY">(),
completedAt: timestamp("completed_at", { withTimezone: true }),
cancelledAt: timestamp("cancelled_at", { withTimezone: true }),
retentionExpiresAt: timestamp("retention_expires_at", { withTimezone: true }),
payload: jsonb("payload").$type<Record<string, unknown>>().notNull().default({}),
}, (table) => ({
instanceTaskUnique: uniqueIndex("onboarding_tasks_instance_task_uq")
.on(table.workflowInstanceId, table.taskKey),
formAssignmentIdx: index("onboarding_tasks_form_assignment_idx").on(table.formAssignmentId),
reminderIdx: index("onboarding_tasks_reminder_idx").on(table.status, table.nextReminderAt),
}));Start onboarding transaction
apps/api/src/coach-client-management/use-cases/start-client-onboarding.ts
ts
@Injectable()
export class StartClientOnboardingUseCase {
constructor(
private readonly relationships: CoachClientRelationshipRepository,
private readonly onboarding: OnboardingWorkflowRepository,
private readonly forms: FormsAssignmentPort,
private readonly outbox: OutboxPort,
private readonly tx: TransactionRunner,
private readonly clock: Clock,
) {}
async execute(command: StartClientOnboardingCommand): Promise<ClientOnboardingStateDto> {
return this.tx.run(async () => {
const relationship = await this.relationships.lockById(command.relationshipId);
relationship.assertCreatedFromInvitation(command.invitationId);
const formAssignment = await this.forms.assignOnboardingQuestionnaire({
relationshipId: relationship.id,
formVersionId: command.formVersionId,
sourceType: "ONBOARDING",
assignedByUserId: command.coachUserId,
});
const workflow = OnboardingWorkflowInstance.startFromRegistration({
relationshipId: relationship.id,
invitationId: command.invitationId,
definitionKey: "CLIENT_INVITE_V1 baseline",
definitionVersion: command.definitionVersion,
questionnaireAssignmentId: formAssignment.id,
now: this.clock.now(),
});
relationship.markOnboarding();
await this.onboarding.save(workflow);
await this.relationships.save(relationship);
await this.outbox.publishAll(workflow.pullEvents());
return ClientOnboardingStateDto.from(workflow);
});
}
}Skip or complete task use case
apps/api/src/coach-client-management/use-cases/update-onboarding-task.ts
ts
@Injectable()
export class UpdateOnboardingTaskUseCase {
constructor(
private readonly onboarding: OnboardingWorkflowRepository,
private readonly submissions: FormsSubmissionQuery,
private readonly policy: OnboardingAccessPolicy,
private readonly tx: TransactionRunner,
private readonly clock: Clock,
) {}
async skip(input: SkipOnboardingTaskInput, actor: Actor): Promise<ClientOnboardingStateDto> {
return this.tx.run(async () => {
const workflow = await this.onboarding.lockByTaskId(input.taskId);
this.policy.assertClientCanAct(actor, workflow.relationshipId);
workflow.skipQuestionnaireForNow({
taskId: input.taskId,
clientUserId: actor.userId,
reason: input.reason,
nextRoute: "LIGHTWEIGHT_WELCOME",
reminderCadence: "DAILY",
now: this.clock.now(),
});
await this.onboarding.save(workflow);
await this.onboarding.saveEventsToOutbox(workflow);
return ClientOnboardingStateDto.from(workflow);
});
}
async completeFromForm(input: CompleteOnboardingTaskInput): Promise<ClientOnboardingStateDto> {
return this.tx.run(async () => {
const workflow = await this.onboarding.lockByTaskId(input.taskId);
const submission = await this.submissions.getAcceptedSubmission(input.formSubmissionId);
workflow.completeQuestionnaireFromSubmission({
taskId: input.taskId,
formAssignmentId: submission.assignmentId,
formSubmissionId: submission.id,
clearAssignmentBlocks: ["PROGRAM", "NUTRITION"],
now: this.clock.now(),
});
await this.onboarding.save(workflow);
await this.onboarding.saveEventsToOutbox(workflow);
return ClientOnboardingStateDto.from(workflow);
});
}
}Events & Background Jobs
Workflow events decouple onboarding from Forms, client profile mapping, notifications, and coach inbox read models.
Domain Events
ClientOnboardingStartedOnboardingQuestionnaireAssignedOnboardingTaskOpenedOnboardingQuestionnaireSkippedOnboardingQuestionnaireSubmittedClientOnboardingCompletedWithPendingTaskClientOnboardingCompleted
Jobs And Side Effects
- Create form assignment after registration transaction commits or inside the same transaction through a Forms application service.
- Listen for
FormSubmissionReceivedto complete the onboarding task idempotently and clear program/nutrition assignment blocks. - Create coach inbox/read-model updates when questionnaire is skipped or submitted.
- Schedule daily reminders for skipped questionnaire tasks until submitted, cancelled, or relationship ended.
- Expose skipped-questionnaire blocks to program and nutrition assignment validation.
- Run explicit form-field mapping jobs after submission.
- Purge or anonymize cancelled questionnaire assignments and onboarding task history after the 30-day post-termination retention window.
Permissions & Access Rules
Access is derived from the authenticated client, coach workspace membership, coach-client relationship, and Forms assignment ownership.
Client
- Can read and act on onboarding tasks for their active relationship only.
- Can skip the questionnaire only during eligible first-run onboarding or while task status allows it.
- Can resume skipped questionnaire without invitation token after authentication.
- Cannot mark questionnaire complete without a valid linked form submission.
Coach
- Can view onboarding status for clients in their workspace relationship.
- Can nudge, cancel, or reassign questionnaire task with audit reason.
- Cannot make the questionnaire non-skippable for a specific client in Public V1.
- Cannot assign programs or nutrition while questionnaire status is
SKIPPED_FOR_NOW. - Cannot impersonate client submission or edit submitted answers through onboarding APIs.
- Can choose invitation questionnaire version only through supported Forms/client invitation flows.
System
- Creates onboarding instances only from validated invitation registration.
- Enforces idempotency across registration, task skip, and task completion.
- Does not expose coach-configurable workflow-template writes or per-client skippability overrides in Public V1.
- Writes outbox/audit events for every task state transition.
Web, Coach Mobile, Client Mobile
V1 baseline keeps workflow editing out of the UI and focuses on clear status, skip/resume behavior, and coach visibility.
Coach Web
- Invitation flow lets coach select the onboarding questionnaire or use workspace default.
- Client profile shows onboarding status: not started, questionnaire pending, skipped, submitted, cancelled.
- Coach can send reminder/nudge, reassign questionnaire, or cancel the pending task with a reason.
- Program and nutrition assignment flows show a blocking skipped-questionnaire state until the questionnaire is submitted or cancelled.
Coach Mobile
- Shows read-only onboarding status and quick actions: message/nudge client.
- Can review submitted questionnaire if Forms review is supported on mobile.
- Full questionnaire/template management can remain web-first for V1 baseline.
Client Mobile
- After account creation, shows questionnaire task with primary action and secondary
Skip for now. - After skip, routes to a lightweight welcome screen while keeping the questionnaire in tasks.
- On app restart, displays pending skipped questionnaire without requiring invitation context.
- Shows daily reminder prompts for skipped questionnaire tasks, respecting notification preferences.
- Requires online acknowledgement for skip and completion in Public V1.
Test Scenarios & V1 Baseline Decisions
Validate the fixed V1 baseline sequence, skip semantics, blocking gates, daily reminders, retention, idempotency, and Forms handoff before implementation.
Tests
- Domain: registration creates one onboarding instance and one questionnaire task per relationship under retry.
- Domain: skip transitions task to
SKIPPED_FOR_NOW, workflow toCOMPLETED_WITH_PENDING_TASK, and next route toLIGHTWEIGHT_WELCOME. - Domain: skip sets daily reminder cadence and blocks program and nutrition assignment.
- Domain: completing from form clears program and nutrition assignment blocks.
- Domain: coach cannot configure per-client non-skippable questionnaire behavior in Public V1.
- API: client cannot skip or complete another relationship's onboarding task.
- API: complete-from-form rejects submissions not tied to the task's form assignment.
- Integration:
FormSubmissionReceivedcompletes skipped or opened task idempotently. - Integration: coach read model updates when questionnaire is skipped and when submitted.
- Integration: program and nutrition assignment APIs reject assignment while questionnaire is skipped and pending.
- Reminder job: skipped questionnaire receives one reminder per day until submitted, cancelled, or relationship ended.
- Retention: cancelled questionnaire assignments and onboarding task history are purged or anonymized after 30 days from relationship termination.
- Mobile E2E: new client registers, skips questionnaire, lands on lightweight welcome, restarts, and resumes questionnaire later.
- Security: invitation token is not required after account registration; authenticated relationship access is required.
Resolved V1 Baseline Decisions
- Post-skip route: client lands on a lightweight welcome screen.
- Skippability: fixed for V1 baseline; coaches cannot make the questionnaire non-skippable for specific clients.
- Skipped reminder cadence: daily until submitted, cancelled, or relationship ended.
- Assignment gate: skipped questionnaire blocks both program and nutrition assignment.
- Retention: cancelled questionnaire assignments and onboarding task history are retained read-only for 30 days after relationship termination, then purged or anonymized from normal access.