Skip to Content
Living documentation — last reviewed 2026-05-28
FeaturesLeads CrmLeads & CRM

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. Taikan 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 Taikan-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 Taikan-marketing-site waitlist; no org attached.
  • lead_status_events — full audit trail of status transitions with from_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

PersonaWhy
Public (unauthed)Submits a minisite contact form (POST /leads/organization/:orgId) or platform waitlist (POST /leads).
OwnerReviews + assigns + converts leads; lead-management is the conversion funnel.
AdminSame as owner; can convert.
CoachSame as owner/admin for read/update; cannot convert (convert requires owner/admin in convertLead).
MemberNo 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 tasks row of type contact_lead, priority high, source auto is created with dueDate=tomorrow and assigneeId=org owner.userId. Surfaces in the dashboard task widget.
  • Conversion creates a membership with source_lead_id set and emits MEMBERSHIP_ACTIVATED { source: 'lead_converted' } — kicks off downstream onboarding (compliance forms fan-out, etc.).

High-level capabilities

  1. Platform leadsPOST /leads (@Public). Inserts leads + platform_leads (waitlist tag).
  2. Minisite lead submitPOST /leads/organization/:orgId (@Public). Org-side dedup, inserts leads + organization_leads. Source forced to minisite.
  3. List / search leadsGET /organizations/:orgId/leads (staff only). Paginated, filterable by status, source, free-text search over name/email/phone.
  4. Lead detailGET /organizations/:orgId/leads/:leadId. Returns the lead + the assignee user info + status events history.
  5. Manual createPOST /organizations/:orgId/leads (staff). Same dedup as minisite path, but with arbitrary source (default manual).
  6. Update leadPATCH /organizations/:orgId/leads/:leadId. Fields: name, email, phone, note, status, assignedToUserId, trialDate. Status transitions emit a lead_status_events row with the actor.
  7. Convert to memberPOST /organizations/:orgId/leads/:leadId/convert (owner/admin). Creates user (or finds by email) + membership with source_lead_id. Marks lead converted. Emits MEMBERSHIP_ACTIVATED.
  8. AnalyticsGET /organizations/:orgId/leads/analytics. Returns total, newThisMonth, converted, conversionRate (rounded %), bySource[], byStatus[].
  9. Auto-ingestion (webhook channels) — connected Meta Lead Ads Pages and WhatsApp numbers auto-create org leads from inbound webhooks, each stamped with the ad/form as its hook. Owner/admin connect their accounts from Settings → Integrations. See lead-ingestion.md.
  10. Configurable pipelines + board…/lead-pipelines*. A user-defined kanban funnel of named, ordered stages; each stage maps to a lifecycle category that keeps leads.status (and conversion/analytics/automations) in sync. Drag to move; into Converted routes through the Convert flow, into Trial booked prompts a date. See ADR-0012.
  11. Unified inbox (email)…/conversations*. A channel-agnostic thread per contact (lead or member). Outbound email via Resend; inbound replies via Cloudflare Email Routing → a zero-dep Worker → /webhooks/email/inbound, threaded by a per-conversation Reply-To. First outbound auto-advances the lead to contacted. Manual notes always available; WhatsApp/SMS adapters later. See the runbook inbox-email-inbound.md.

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 | lost

Relationship to other features

  • membershipsconvertLead inserts a membership with source_lead_id. Emits MEMBERSHIP_ACTIVATED { source:'lead_converted' }.
  • users-authconvertLead finds-or-creates a user shell by email.
  • organizationsorganization_leads.organization_id is the direct org boundary.
  • Tasks (out-of-bucket) — contact_lead auto-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.
  • Lead ingestion (in-bucket) — Meta Lead Ads + WhatsApp webhooks auto-create leads, routed per-org via integration_connections; the hook/attribution lives in lead_campaigns + lead_attribution.

Current status

Core CRM shipped. Recent additions: status history (lead_status_events), auto-ingestion, and — most recently — configurable pipelines + the unified email inbox (this PR). Ingestion: Meta Lead Ads is live (PR #137); WhatsApp Cloud is built but gated on Meta App Review (PR #138). The inbox ships the email channel + manual notes; WhatsApp/SMS adapters are deferred. See lead-ingestion.md and inbox-email-inbound.md.

Known gaps

  • convertLead is the only place where a members row of type lead_converted is created. There is no “convert without a paying plan” subscription — converted members start with status='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).