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_TYPESand that havesaveToClienton are written to the trainee:heighttoClient.heightCm,birth_datetoClient.birthDate,goaltoClient.goal(the option’s label),weightto aWeightEntrywithsource: 'form'andformResponseId,body_measurementmerged intoDailyMetric.measurements,image_uploadto aClientPhotowith the field’sconfig.pose. - A
food_preferencesanswer updatesClient.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:
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.