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

sequenceDiagram participant Admin participant API as POST /api/admin/bookings/:id/attendance participant DB as D1 participant Email as Resend participant Parent

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:

StageState
Created (admin records attendance + sends link)used_at = null
Parent opens the linkValidated: exists + used_at IS NULL + not expired
Parent submits the formused_at = now()
Any subsequent submissionRejected: 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:

FieldDescription
first_name / last_nameAthlete name
dobAthlete date of birth
emailMembership contact email
mobile_phoneContact mobile number
whatsapp_opt_inyes / no — WhatsApp group consent
consent_data_processingyes / no — GDPR consent
consent_policiesyes / no — Club policies consent
emergency_contact_nameEmergency contact name
emergency_contact_mobileEmergency contact mobile
existing_family_memberyes / no — family member already a club member
existing_family_member_detailsFree 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.

Athlete journey — full lifecycle

flowchart LR A([Enquiry]) --> B{Route} B -->|Taster| C[Booking Invite\nemail sent] B -->|Academy| W[Waitlist\nemail sent] C --> D[Parent books\n/book/:token] D --> E[Confirmed booking\nConfirmation email] E --> F[7-day reminder\ncron sends email] F --> G[Session day] G --> H{Admin records\nattendance} H -->|attended| I[Membership invite\nsent] H -->|no_show| J[No further action] I --> K[Parent completes\n/join?token=...] K --> L[Member] W --> M[Season invite\nAdmin sends] M --> N{Parent\nresponds} N -->|yes| O[Accepted] N -->|no| P[Declined]