Background
Tournament onboarding needs orchestration that Forms deliberately doesn't provide: deciding which forms a member must complete to be considered onboarded, in what order, and what to do once they've finished them all. This is a separate feature sitting on top of Forms, not a sub-feature of it — Onboarding references Forms constructs (form_id), Forms never references Onboarding, with one deliberate, narrow exception (see Proposed changes).
The original scope for this issue (an OnboardingPhase/OnboardingPhaseForm model with typed visibility conditions and a TournamentMembershipTrackStatus table) turned out to be more than the actual need called for. It's been replaced during implementation with a flatter model, described below. Most of it has since shipped.
Motivation
TDs need to change what's required to become an official tournament member as the season progresses (e.g. fall: test-writing interest + day-of interest; winter: day-of interest only; pre-tournament: confirmation + logistics) without engineering involvement each time, and without members who already cleared an earlier requirement set being incorrectly bounced back to "incomplete."
Proposed changes
Backend model — TournamentForm: a 1:1 companion row every tournament-scoped Form gets at creation (backend/app/models/models.py). is_onboarding (bool) + order (int, set only for onboarding rows) drive the sequence; form_id is its own primary key. TournamentMembership.onboarded_at (nullable timestamp) snapshots completion — set once a member has answered every currently-onboarding-flagged published form, and permanent from then on: removing/archiving an onboarding form later never unsets it, only adding a new onboarding form clears it tournament-wide (a genuine expansion of the requirement, unlike passive list churn).
Backend routes (backend/app/api/routes/tournament/onboarding.py):
- TD management:
GET/POST /tournaments/{id}/onboarding-forms/, PATCH .../reorder/, DELETE .../{form_id}/. Add requires the form be published; reorder validates a contiguous 1..N sequence.
- Member-facing:
POST /tournaments/{id}/onboarding/progress/ — finds the next unanswered published onboarding form, snapshots onboarded_at once none remain.
- One exception to the Forms/Onboarding dependency direction:
_reject_if_onboarding in backend/app/api/routes/forms.py blocks archiving or hard-deleting a form still flagged is_onboarding — it must be removed from onboarding first.
Frontend — drag-and-drop config page under tournament Forms → Onboarding tab (frontend/app/dashboard/tournaments/[id]/forms/onboarding/page.tsx); /join now routes into /tournaments/{id}/onboarding, which resolves to the next required form and advances automatically on submit (frontend/app/tournaments/[id]/onboarding/**).
Design decisions replacing the original scope:
- No
OnboardingPhase — a phase is just how a TD talks about the sequence, not a modeled concept. One flat, ordered list of onboarding forms, managed directly.
- No visibility conditions for onboarding forms — order + published status is the only gate. Role/availability-based gating is deferred to standard (non-onboarding) forms as a
prerequisites mechanism, not built yet.
- No
creates_membership_on_submit side-effect — unnecessary, since a TournamentMembership already exists (status="interested") from the moment someone joins, before onboarding starts.
What will not change
- Forms itself (
Form/FormField/FormResponse/FormAnswer, question types, branching, write-through mechanics) stays unaware of Onboarding, aside from the one archive/delete guard above.
Completion criteria
TournamentForm model + migration (with backfill for pre-existing forms)
- Archive/delete guard for onboarding-linked forms
- TD management routes (add / reorder / remove)
- Member progress endpoint +
onboarded_at snapshot semantics
- Drag-and-drop config page
/join routes into the onboarding flow and advances on submit
- Test coverage for config CRUD and member progress (
backend/tests/api/tournament/test_onboarding.py)
Background
Tournament onboarding needs orchestration that Forms deliberately doesn't provide: deciding which forms a member must complete to be considered onboarded, in what order, and what to do once they've finished them all. This is a separate feature sitting on top of Forms, not a sub-feature of it — Onboarding references Forms constructs (
form_id), Forms never references Onboarding, with one deliberate, narrow exception (see Proposed changes).The original scope for this issue (an
OnboardingPhase/OnboardingPhaseFormmodel with typed visibility conditions and aTournamentMembershipTrackStatustable) turned out to be more than the actual need called for. It's been replaced during implementation with a flatter model, described below. Most of it has since shipped.Motivation
TDs need to change what's required to become an official tournament member as the season progresses (e.g. fall: test-writing interest + day-of interest; winter: day-of interest only; pre-tournament: confirmation + logistics) without engineering involvement each time, and without members who already cleared an earlier requirement set being incorrectly bounced back to "incomplete."
Proposed changes
Backend model —
TournamentForm: a 1:1 companion row every tournament-scopedFormgets at creation (backend/app/models/models.py).is_onboarding(bool) +order(int, set only for onboarding rows) drive the sequence;form_idis its own primary key.TournamentMembership.onboarded_at(nullable timestamp) snapshots completion — set once a member has answered every currently-onboarding-flagged published form, and permanent from then on: removing/archiving an onboarding form later never unsets it, only adding a new onboarding form clears it tournament-wide (a genuine expansion of the requirement, unlike passive list churn).Backend routes (
backend/app/api/routes/tournament/onboarding.py):GET/POST /tournaments/{id}/onboarding-forms/,PATCH .../reorder/,DELETE .../{form_id}/. Add requires the form bepublished; reorder validates a contiguous 1..N sequence.POST /tournaments/{id}/onboarding/progress/— finds the next unanswered published onboarding form, snapshotsonboarded_atonce none remain._reject_if_onboardinginbackend/app/api/routes/forms.pyblocks archiving or hard-deleting a form still flaggedis_onboarding— it must be removed from onboarding first.Frontend — drag-and-drop config page under tournament Forms → Onboarding tab (
frontend/app/dashboard/tournaments/[id]/forms/onboarding/page.tsx);/joinnow routes into/tournaments/{id}/onboarding, which resolves to the next required form and advances automatically on submit (frontend/app/tournaments/[id]/onboarding/**).Design decisions replacing the original scope:
OnboardingPhase— a phase is just how a TD talks about the sequence, not a modeled concept. One flat, ordered list of onboarding forms, managed directly.prerequisitesmechanism, not built yet.creates_membership_on_submitside-effect — unnecessary, since aTournamentMembershipalready exists (status="interested") from the moment someone joins, before onboarding starts.What will not change
Form/FormField/FormResponse/FormAnswer, question types, branching, write-through mechanics) stays unaware of Onboarding, aside from the one archive/delete guard above.Completion criteria
TournamentFormmodel + migration (with backfill for pre-existing forms)onboarded_atsnapshot semantics/joinroutes into the onboarding flow and advances on submitbackend/tests/api/tournament/test_onboarding.py)