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_captureblock so the UI exposes the contact form. Without that block the dashboard never logs a lead. - Normalized lead email: When
POST /api/submissionsreceivesleadEmailandsubmissionId, the API trims, lowercases, and validates the email before persisting it aslead_emailon 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_emailinsubmissionsfor the owner within the current billing window (src/app/api/leads/route.ts). Once the monthly lead limit is reached, the submission still succeeds, butlead_emailis stored asnullso 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 theleadstable. The service always usesupsertwith the(quiz_id, submission_id)conflict target to update the existing row instead of inserting a second copy.
How dashboards surface leads
- Leads card:
DashboardLayoutqueriesleadsfor every quiz you own, filters on the billing-period start (UTC month for free plans, billing cycle for paid), and shows that count in theLeads this monthcard, theLeadsSubmissionsChart, and every “Leads” drill-down. - Per-quiz view:
Dashboard > Quizzes > Leadslists the most recent 100 rows from theleadstable, 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 theleadstable to keep the chart aligned with the cards and the limit enforcement described above.
Note
The dashboard only counts what is stored in leads. If you expect a lead but the card stays at zero, double-check whether the lead capture block executed, whether the normalized email was valid, and whether the monthly lead limit had already been reached.
Why duplicates do not inflate the dashboard
- Unique by submission: Every lead is tied to a
submission_id, and the database enforcesleads_quiz_submission_unique(supabase/migrations/20251227_add_leads.sql). A retry or resubmission that carries the samesubmissionIdsimply updatesupdated_at(andnotification_sent_atwhen notifications fire) instead of creating a second row. - Upsert from the API: Both
POST /api/submissionsandPOST /api/leadscallupsert(..., { onConflict: "quiz_id,submission_id" }), so duplicates are merged before the dashboard query runs. Without asubmission_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,
resolvedLeadEmailis set tonulland neither theleadstable 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
leadEmailto persist. - Owner testing or retries without
submission_id: Owner sessions skip lead persistence entirely, and retries that omit the originalsubmission_idcreate a fresh lead row even if the contact is the same person.

