A coach builds a FormTemplate. Sending it to a trainee creates a FormAssignment. The trainee’s answers land in a FormResponse. For check-in forms the coach’s follow-up is recorded in a CheckinReview. The JSON inside schema and answers is documented in Form schema JSON.

FormTemplate

Table form_templates. Relations: responses, assignments, automationHooks, and four named back-relations that say where the form is wired in: All four are SetNull on the referencing side, so deleting a form clears the pointers.

FormType

FormStatus

DRAFT, PUBLISHED, ARCHIVED.

FormCadence

WEEKLY, BIWEEKLY, MONTHLY. The same enum is used on Client.updateCadence. The next-date math is mirrored between the web app and the backend, so change both.

DefaultFormTemplate

Table default_form_templates. Global starter forms managed by super admins. No tenant. Every newly provisioned studio receives a copy of the active rows. Editing a row here affects only studios created afterwards.

FormAssignment

Table form_assignments. One send of a form to one trainee. Indexes: studioId, (clientId, status), formTemplateId, (studioId, clientId, createdAt), (status, notifiedAt).

FormAssignmentStatus

PENDING, COMPLETED, CANCELLED.

FormResponse

Table form_responses. A submitted form. Indexes: formTemplateId, assignmentId, clientId, (clientId, submittedAt), (clientId, createdAt). There is no studioId. Scope through the client or the template.

Side effects of a submission

forms.service.ts does more than insert the row:
  • Answers of fields whose type is in CLIENT_SAVE_TYPES and that have saveToClient on are written to the trainee: height to Client.heightCm, birth_date to Client.birthDate, goal to Client.goal (the option’s label), weight to a WeightEntry with source: 'form' and formResponseId, body_measurement merged into DailyMetric.measurements, image_upload to a ClientPhoto with the field’s config.pose.
  • A food_preferences answer updates Client.foodPreferences.
  • The form-filled automation and outbound webhook fire. See Outbound webhooks.

signingAudit

Written by apps/core-api/src/modules/form-sign/form-sign.service.ts:

Document signing flow

PDF_SIGNATURE forms are signed on a public page, with no user session. The 128-bit signToken in the path is the credential. The router is mounted at /v1/public/sign (PUBLIC_SIGN_MOUNT): Responses on this lane are plain JSON, not the usual ok and data envelope, because the web app’s proxy relays them to the page as they are. The lane keeps its own per-link and per-visitor rate counters, since every request reaches the API from the proxy’s single address. A forwarded client IP is believed only on a request the proxy signed. On submit the service stamps the values and signatures onto the PDF, stores the result, and creates the FormResponse with documentUrl and signingAudit.

CheckinReview

Table checkin_reviews. One check-in queue review per submitted form. This model has no relations on purpose. It is fully separate from the Work Board’s FORM_FILLED tasks.

draft

Validated by draftBody in apps/core-api/src/modules/checkins/checkins.schema.ts:
A draft may be half-written. Only the shape is enforced so autosave never bounces. Completing the review uses feedbackBody, which adds the real rules: with sendToTrainee on, a message and at least one channel are required. Without it, the internal note must be non-empty. Older web clients send the legacy shape without sendToTrainee. The withLegacySend preprocess reads a non-empty message or channel list as “send”. Stored drafts are normalized the same way on read.

FormReminderSend

Table form_reminder_sends. The ledger behind form reminders. Unique on (instanceKey, messageId). Each configured reminder message is sent at most once per form instance. The reminder engine inserts first and treats a unique violation as “already sent”. The messages themselves are configured in NotificationTypeConfig.messages and StudioNotificationConfig.messages for the FORM_SENT and FORM_REMINDERS types. See Messaging and notifications.