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 Attendance API
participant DB as D1
participant Email as Resend
participant Parent
Admin->>API: attended + send_membership_link=true
API->>DB: UPDATE booking SET status=attended
API->>DB: Append event: attendance_recorded
API->>DB: INSERT membership_otps\ntoken + used_at=null
API->>Email: sendMembershipInvite
Email-->>Parent: Membership invite email
API-->>Admin: membership_invite_sent
Parent->>DB: Open membership form link
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.
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\nvia invite link]
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\nmembership form]
K --> L[Member]
W --> M[Season invite\nAdmin sends]
M --> N{Parent\nresponds}
N -->|yes| O[Accepted]
N -->|no| P[Declined]