Skip to Content
Living documentation — last reviewed 2026-05-28
FeaturesFormsForms (Compliance + Check-ins)

Forms (Compliance + Check-ins)

Linear epic: FIT-176 (Forms engine) · FIT-158 (Hebrew compliance PDFs) · FIT-178 (Hebrew signing UX) · FIT-184 (Engine design spike) · FIT-189 (Public token-gated upload presign) Status: Backend feature-complete; staff UI shipped; public signing surface (FIT-178) and member in-app signing UI are pending and about to enter QA. Last reviewed: 2026-05-28

What

A unified forms engine that drives two distinct domains over a shared template/instance schema:

  1. Compliance forms (this bucket’s focus): legally compelling Hebrew waivers that Israeli physical-premises gyms must collect — health declarations, club regulations, parental consent for minors, freeze/cancellation requests, studio rental agreements. Output is a signed, immutable PDF in R2 with a SHA-256 checksum and an audit footer (token id, IP, user-agent, timestamp).
  2. Check-in forms: recurring coach-driven questionnaires (mood, sleep, sRPE) that fan out on a schedule and produce answers, not PDFs. Documented under check-ins; the engine code is shared.

This README and the rest of this folder cover the compliance half in depth; check-ins are referenced where they share state machine surface but live in docs/features/onboarding/ and docs/features/insights/ discussions.

Why

Israeli law (חוק לקידום הספורט / Sports Promotion Law) requires gyms to obtain a health declaration before granting access, and the standard membership contract / freeze / cancellation flows are paper-driven across most Israeli studios. FitKit replaces the clipboard with an electronic signature flow that:

  • renders Hebrew RTL with native font shaping (no PDFKit glyph hacks);
  • pins the signed bytes by SHA-256 and stores them in a dedicated R2 bucket with planned 7-year retention;
  • works on a member’s phone (in-app, authenticated) or via a single-use WhatsApp-sharable link for the non-app cases (a parent of a minor, an external studio renter).

A compliance form is unique among FitKit features because the artefact must outlive the platform: even if the gym leaves FitKit, the signed PDFs are legally meaningful evidence and must be downloadable indefinitely.

Who (personas)

PersonaSurfaceCapabilities
Org owner / admin / coach/dashboard/forms (web)Create templates, edit drafts, publish, archive, bump versions, assign to one member, bulk-assign to “everyone missing”, view coverage, mint signing links, download signed PDFs
MemberMobile app (primary) + /forms/mine (web — pending)View own pending instances, draw signature, submit (authenticated)
Unauthenticated signer/[lang]/forms/sign/{token} (pending — FIT-178)Open token-gated link, draw signature, submit. Used for parents of minors and external studio renters

Capabilities (current)

  • Template CRUD — staff create draft → patch → publish; published rows are immutable except via bumpVersion which forks a new row at version+1 while keeping old in-flight instances pinned.
  • Six hardcoded compliance type-keyshealth_declaration, club_regulations, parental_consent_youth, membership_freeze, membership_cancellation, studio_rental_agreement. Validated server-side via the Zod enum complianceTypeKeyEnum (libs/shared/src/lib/schemas/forms.ts:31).
  • Hebrew presets — opinionated starter templates for the six type-keys in libs/shared/src/lib/schemas/forms-presets.ts, aligned with PAR-Q-style health questions and standard Israeli gym contract clauses.
  • Auto-issue on join — staff toggle autoIssueOnJoin on a compliance template; the engine listens for MEMBERSHIP_ACTIVATED events (FormsService.handleMembershipActivated, forms.service.ts:365) and issues pending instances for every newly-active member (not staff). Idempotent: skips type-keys the member already has covered.
  • Bulk coverage — for any compliance template, return the set of active members missing or expired against that typeKey (any version) and fan out pending instances in one transaction.
  • Two signing paths:
    • Authenticated: member signs in-app from their forms/mine list. Identity verified against the Clerk session.
    • Token-gated: staff explicitly mints a 7-day single-use signing token. Public route /forms/sign/:token. No auth — the 64-hex token is the credential.
  • PDF render pipeline — Puppeteer + @sparticuz/chromium headless render of a Hebrew RTL HTML template with org logo, signature image, and audit footer. SHA-256 of the bytes is stored alongside the R2 key.
  • Dedicated R2 bucketR2_COMPLIANCE_BUCKET_NAME (falls back to default in dev). The signature image PNG lives in the regular bucket (it’s an input, not a legal artefact); the PDF lives in the compliance bucket.
  • Staff PDF retrieval — presigned 30-day GET URL, cached in Redis with TTL < signed expiry.
  • Online org guard — compliance is rejected at the service boundary for organization.type = 'online'. Online studios don’t have a premises problem so the compliance UI is hidden and the API refuses direct hits.

Capabilities (gaps + tracking)

GapLinear
Public signing page (/[lang]/forms/sign/{token}) — Hebrew RTL signature canvas, submit, success stateFIT-178
Member in-app signing screen + push deep-link targetFIT-178
Audit logging — audit_log table with actor_id, action, entity_id, metadata. Today, signature audit is the per-row metadata on form_signatures onlyFIT-20
R2 compliance-bucket object lock + 7-year lifecycle policy at the infra layerFIT-158
Push notification on issue + reminder cadence for pending complianceFIT-170 (engine), forms-specific category TBD
Re-render / re-sign on version bump (currently old versions stay signed; no auto-issue of v+1)not tracked
Soft-delete / redaction policy on member account deletion (PII inside the PDF is currently retained — see behavior.md invariants)not tracked
WhatsApp delivery integration (today, staff copy/paste the URL)FIT-158 deferred
  • MembershipsMEMBERSHIP_ACTIVATED event drives auto-issue; see apps/api/src/memberships/membership-events.ts. Documented at docs/features/memberships/.
  • Legal — separate domain (TOS, privacy, fitness waiver) tracked under legal_documents / legal_consents. See docs/features/legal/. Forms are gym-issued; legal is platform-issued.
  • Uploads / R2 — see docs/features/uploads-r2/. The signature PNG upload uses the regular staged-PUT flow when authenticated; tokenized signing uses a bespoke public presign (FIT-189).
  • Push notifications — see docs/features/push-notifications/. Compliance issuance does NOT push yet; check-in fan-out does.
  • Onboarding — auto-issue plugs into the activate-membership step.
  • Member detail page/dashboard/members/{id} exposes the per-member compliance audit tab via member-forms-tab.tsx.

Source code anchor

LayerPath
API serviceapps/api/src/forms/forms.service.ts
API controllersapps/api/src/forms/forms.controller.ts (auth + public)
PDF rendererapps/api/src/forms/pdf.service.ts
DTOsapps/api/src/forms/dto/{create,update,assign-compliance,submit-form-answers}-form.dto.ts
DB schemalibs/db/src/lib/schema/forms.ts
Shared types + state machinelibs/shared/src/lib/schemas/forms.ts
Hebrew presetslibs/shared/src/lib/schemas/forms-presets.ts
Staff web UIapps/web/src/components/overview/forms/ and apps/web/src/app/[lang]/(protected)/dashboard/forms/
Member detail audit tabapps/web/src/components/overview/members/member-forms-tab.tsx
Mobile member UIfitkit-mobile (separate repo): in-app at app/(tabs)/profile/ “My Forms” via src/hooks/use-forms.ts; token-gated at app/forms/sign/[token].tsx via src/hooks/use-form-token.ts; renderer at src/components/forms/form-renderer.tsx. See FIT-178 and docs/architecture/mobile.md.
R2 clientapps/api/src/r2/r2.service.ts

See code-map.md for the full inventory and behavior.md for the state machine + golden paths.