Leads & CRM
What is this
A lead is a prospective customer — someone who filled out a minisite form, walked in, came from Instagram, etc. — who isn’t yet a paying member. FitKit splits leads into two layers:
leads— the canonical lead row: name, email, phone, locale, source, status, note. Used for both platform leads (people interested in FitKit-the-platform) and organization leads (people interested in a specific studio).organization_leads— the per-studio link that attaches a lead to an organization, an optional assignee (staff user), an optional trial date, and the converted membership (when the lead becomes a member).platform_leads— the FitKit-marketing-site waitlist; no org attached.lead_status_events— full audit trail of status transitions withfrom_status,to_status,changed_by_user_id.
The CRM is gated behind the lead_management platform feature (@RequiresFeature('lead_management') decorator on the org-leads controller) — only orgs on tiers that include this feature can use it.
Who uses it
| Persona | Why |
|---|---|
| Public (unauthed) | Submits a minisite contact form (POST /leads/organization/:orgId) or platform waitlist (POST /leads). |
| Owner | Reviews + assigns + converts leads; lead-management is the conversion funnel. |
| Admin | Same as owner; can convert. |
| Coach | Same as owner/admin for read/update; cannot convert (convert requires owner/admin in convertLead). |
| Member | No access. |
Persona impact
- Public minisite forms are server-side de-duplicated against this org’s existing leads by email+phone. Duplicates return 409 with
existingLeadId. - Auto-task creation — on every new org-lead, a
tasksrow of typecontact_lead, priorityhigh, sourceautois created withdueDate=tomorrowandassigneeId=org owner.userId. Surfaces in the dashboard task widget. - Conversion creates a membership with
source_lead_idset and emitsMEMBERSHIP_ACTIVATED { source: 'lead_converted' }— kicks off downstream onboarding (compliance forms fan-out, etc.).
High-level capabilities
- Platform leads —
POST /leads(@Public). Insertsleads+platform_leads(waitlist tag). - Minisite lead submit —
POST /leads/organization/:orgId(@Public). Org-side dedup, insertsleads+organization_leads. Source forced tominisite. - List / search leads —
GET /organizations/:orgId/leads(staff only). Paginated, filterable bystatus,source, free-textsearchover name/email/phone. - Lead detail —
GET /organizations/:orgId/leads/:leadId. Returns the lead + the assignee user info + status events history. - Manual create —
POST /organizations/:orgId/leads(staff). Same dedup as minisite path, but with arbitrary source (defaultmanual). - Update lead —
PATCH /organizations/:orgId/leads/:leadId. Fields:name,email,phone,note,status,assignedToUserId,trialDate. Status transitions emit alead_status_eventsrow with the actor. - Convert to member —
POST /organizations/:orgId/leads/:leadId/convert(owner/admin). Creates user (or finds by email) + membership withsource_lead_id. Marks leadconverted. EmitsMEMBERSHIP_ACTIVATED. - Analytics —
GET /organizations/:orgId/leads/analytics. Returnstotal,newThisMonth,converted,conversionRate(rounded %),bySource[],byStatus[].
Lead source / status enums
lead_source: minisite | qr | instagram | whatsapp | facebook | website | referral
| walk_in | phone_call | manual | course_purchase
lead_status: new | contacted | trial_booked | converted | lostRelationship to other features
- memberships —
convertLeadinserts a membership withsource_lead_id. EmitsMEMBERSHIP_ACTIVATED { source:'lead_converted' }. - users-auth —
convertLeadfinds-or-creates a user shell by email. - organizations —
organization_leads.organization_idis the direct org boundary. - Tasks (out-of-bucket) —
contact_leadauto-task on new lead;cancellation_review,reengagement, etc. are separate task types. - Platform tiers — controller-level
@RequiresFeature('lead_management')decorator. Orgs without this feature get 403 on every endpoint. - Minisite (out-of-bucket) — public contact form posts here.
Current status
Shipped. The status history (lead_status_events) is the recent addition; getLeadById returns the full event list.
Known gaps
convertLeadis the only place where amembersrow of typelead_convertedis created. There is no “convert without a paying plan” subscription — converted members start withstatus='active', payment_status='none'. The first subscription/plan must be assigned separately.- Auto-task creation is best-effort — wrapped in a try/catch that swallows errors so the lead creation never fails.
- No bulk operations on leads (bulk-assign, bulk-status, bulk-convert).
- No “merge leads” workflow — the dedup check returns 409 with
existingLeadId; the UI must direct the user to the existing record. - Status events are insert-only (no edit/delete of history).