Docs

Lead Definition & Duplicate Handling

This page walks through the exact criteria QuizFlow Labs uses to treat a submission as a lead, how those rows become the Leads this month value and drill-down views, and why replays or retries of the same submission never count twice.

When a submission becomes a lead

  • Lead capture block required: The quiz definition must include a lead_capture block so the UI exposes the contact form. Without that block the dashboard never logs a lead.
  • Normalized lead email: When POST /api/submissions receives leadEmail and submissionId, the API trims, lowercases, and validates the email before persisting it as lead_email on the submission row. Owner tests, blank emails, or invalid addresses are ignored so they do not become leads.
  • Limit-aware persistence: Before saving the lead, the server counts every lead_email in submissions for the owner within the current billing window (src/app/api/leads/route.ts). Once the monthly lead limit is reached, the submission still succeeds, but lead_email is stored as null so no lead row is inserted.
  • Lead table upsert: When a lead is created, QuizFlow Labs writes the details (quiz_id, submission_id, contact fields, consent, timestamps) to the leads table. The service always uses upsert with the (quiz_id, submission_id) conflict target to update the existing row instead of inserting a second copy.

How dashboards surface leads

  • Leads card: DashboardLayout queries leads for every quiz you own, filters on the billing-period start (UTC month for free plans, billing cycle for paid), and shows that count in the Leads this month card, the LeadsSubmissionsChart, and every “Leads” drill-down.
  • Per-quiz view: Dashboard > Quizzes > Leads lists the most recent 100 rows from the leads table, so any persisted duplicate would appear there once and not again.
  • Analytics API: Paid charts rely on /api/analytics/funnel-timeseries, which also tallies leads directly from the leads table to keep the chart aligned with the cards and the limit enforcement described above.

Why duplicates do not inflate the dashboard

  • Unique by submission: Every lead is tied to a submission_id, and the database enforces leads_quiz_submission_unique (supabase/migrations/20251227_add_leads.sql). A retry or resubmission that carries the same submissionId simply updates updated_at (and notification_sent_at when notifications fire) instead of creating a second row.
  • Upsert from the API: Both POST /api/submissions and POST /api/leads call upsert(..., { onConflict: "quiz_id,submission_id" }), so duplicates are merged before the dashboard query runs. Without a submission_id, each POST will create a new lead.
  • Notifications stay single: To avoid duplicate alerts, the system checks for an existing lead record before sending notifications and only notifies on the first persist. That same check is why you can safely retry submissions without worrying about extra dashboard counts.

When a lead never shows up

  • Lead limit hit: Once the owner’s plan limit is reached, resolvedLeadEmail is set to null and neither the leads table nor the dashboard increment, even though the submission itself still counts toward response limits.
  • Skipped lead capture: Conditional logic or hard-gates can send respondents past the capture block, so there is no leadEmail to persist.
  • Owner testing or retries without submission_id: Owner sessions skip lead persistence entirely, and retries that omit the original submission_id create a fresh lead row even if the contact is the same person.

Related resources