Membership Flow
Membership is the final stage of the athlete journey. After a parent attends a taster session and the admin marks attendance as attended, a one-time membership link can be sent. The parent then completes the club membership form at /join?token=....
End-to-end flow
Admin->>API: status:attended, send_membership_link:true API->>DB: UPDATE booking SET status=attended API->>DB: Append event: attendance_recorded API->>DB: INSERT membership_otps\ntoken=32 random bytes, used_at=null API->>Email: sendMembershipInvite\ncontact_name, membership_url Email—>>Parent: Email with /join?token=… API—>>Admin: membership_invite_sent:true
Parent->>DB: Open /join?token=… Note over DB: Validate OTP exists and unused Parent->>DB: POST form submission DB->>DB: INSERT membership_form_submissions DB->>DB: UPDATE membership_otps SET used_at=now DB->>DB: Append event: membership_form_submitted
OTP lifecycle
Membership invites use a single-use token stored in membership_otps:
| Stage | State |
|---|---|
| Created (admin records attendance + sends link) | used_at = null |
| Parent opens the link | Validated: exists + used_at IS NULL + not expired |
| Parent submits the form | used_at = now() |
| Any subsequent submission | Rejected: OTP already used |
The OTP has a 7-day TTL enforced by isMembershipOtpExpired() in src/lib/business/tokens.ts.
export const MEMBERSHIP_OTP_TTL_DAYS = 7;
export function isMembershipOtpExpired(createdAt: string): boolean {
return isTokenExpired(createdAt, MEMBERSHIP_OTP_TTL_DAYS);
}
Membership form fields
The /join page collects the following, stored as JSON in membership_form_submissions.payload_json:
| Field | Description |
|---|---|
first_name / last_name | Athlete name |
dob | Athlete date of birth |
email | Membership contact email |
mobile_phone | Contact mobile number |
whatsapp_opt_in | yes / no — WhatsApp group consent |
consent_data_processing | yes / no — GDPR consent |
consent_policies | yes / no — Club policies consent |
emergency_contact_name | Emergency contact name |
emergency_contact_mobile | Emergency contact mobile |
existing_family_member | yes / no — family member already a club member |
existing_family_member_details | Free text if yes |
Data access
Membership submissions are accessible in the admin panel at /admin/memberships and via:
GET /api/admin/memberships → JSON list
GET /api/admin/memberships?format=csv → CSV download
The CSV export includes all fields from the payload plus the enquiry name and email for cross-referencing.