Build a sales funnel visualization app in Lovable with Supabase. You'll get a funnel chart showing conversion rates between stages, a stage transition audit trail, Cards for each stage's metrics, and a lead detail Sheet — all backed by Postgres with RLS so each team sees only its own pipeline data.
What you're building
A sales funnel app in Lovable models your pipeline as a series of named stages — from first contact to closed deal. Two Supabase tables power the app: leads stores the current state of each lead (stage, value, owner, source), and stage_transitions records every time a lead moves between stages, creating a permanent audit trail.
The funnel chart uses Recharts FunnelChart to render a top-down funnel shape where each layer's width is proportional to the number of leads at that stage. Hovering over a layer shows the stage name, lead count, total value, and the conversion rate from the previous stage.
Metric Cards below the chart break down each stage independently so managers can spot bottlenecks at a glance. A lead count that drops sharply between two stages indicates where the funnel needs attention. The DataTable below the cards lets reps filter by stage, source, or date range and click into any lead's full detail Sheet.
Final result
A visual sales funnel with conversion rate analytics, a full audit trail of lead movements, and a lead management table — ready to share with your sales team from Lovable's publish URL.
Tech stack
Prerequisites
- Lovable Pro account
- Supabase project created at supabase.com with URL and anon key ready
- VITE_SUPABASE_URL and VITE_SUPABASE_ANON_KEY added to Cloud tab → Secrets
- A list of your actual funnel stage names (or use the defaults: Awareness, Interest, Consideration, Intent, Evaluation, Purchase)
- Basic familiarity with Lovable's Cloud tab and the Publish flow
Build steps
Scaffold the leads schema with stage transitions
Ask Lovable to create the two core tables plus a Postgres function that records a transition row automatically whenever a lead's stage changes. This ensures the audit trail is always complete.
1Create a sales funnel app with Supabase. Set up these tables:23- leads: id, org_id, name, email, company, source (organic|paid|referral|outbound), stage (text), value (numeric), owner_id, notes (text), next_action (text), next_action_date (date), created_at, updated_at45- stage_transitions: id, lead_id, from_stage (text, nullable), to_stage (text), transitioned_by (uuid references auth.users), transitioned_at (timestamptz default now())67Create a Postgres trigger on leads: after UPDATE of stage, insert a row into stage_transitions with from_stage = OLD.stage and to_stage = NEW.stage.89Enable RLS on both tables. For leads: users can read/write rows where org_id matches their profile org_id. For stage_transitions: users can read rows where lead_id is in (SELECT id FROM leads WHERE org_id = user's org_id).1011Seed with 30 sample leads spread across 6 stages: Awareness, Interest, Consideration, Intent, Evaluation, Purchase.Pro tip: Ask Lovable to also create a Postgres view called funnel_metrics that returns stage name, count of leads, sum of value, and the conversion rate from the prior stage — this view will power both the funnel chart and the metric cards with a single query.
Expected result: Lovable creates both tables, the trigger function, and the seed data. Running SELECT * FROM funnel_metrics in the Supabase SQL editor returns one row per stage with counts and conversion rates.
Build the Recharts funnel chart
Render the funnel chart using Recharts FunnelChart component, reading data from the funnel_metrics view. Each layer shows the stage name, lead count, and conversion rate as a tooltip.
1import { useQuery } from '@tanstack/react-query'2import { supabase } from '@/integrations/supabase/client'3import { FunnelChart, Funnel, Tooltip, LabelList, ResponsiveContainer } from 'recharts'4import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'56type FunnelMetric = {7 stage: string8 lead_count: number9 total_value: number10 conversion_rate: number11}1213const STAGE_COLORS = ['#6366f1', '#8b5cf6', '#a78bfa', '#c4b5fd', '#ddd6fe', '#ede9fe']1415export function SalesFunnelChart() {16 const { data: metrics = [], isLoading } = useQuery<FunnelMetric[]>({17 queryKey: ['funnel_metrics'],18 queryFn: async () => {19 const { data, error } = await supabase.from('funnel_metrics').select('*')20 if (error) throw error21 return data22 },23 })2425 const chartData = metrics.map((m, i) => ({26 value: m.lead_count,27 name: m.stage,28 fill: STAGE_COLORS[i % STAGE_COLORS.length],29 conversionRate: m.conversion_rate,30 totalValue: m.total_value,31 }))3233 if (isLoading) return <div className="h-64 animate-pulse bg-muted rounded-lg" />3435 return (36 <Card>37 <CardHeader><CardTitle>Sales Funnel</CardTitle></CardHeader>38 <CardContent>39 <ResponsiveContainer width="100%" height={360}>40 <FunnelChart>41 <Tooltip42 formatter={(value, name, props) => [43 `${value} leads · $${props.payload.totalValue.toLocaleString()} · ${props.payload.conversionRate}% conv.`,44 props.payload.name,45 ]}46 />47 <Funnel dataKey="value" data={chartData} isAnimationActive>48 <LabelList position="center" fill="#fff" stroke="none" dataKey="name" />49 </Funnel>50 </FunnelChart>51 </ResponsiveContainer>52 </CardContent>53 </Card>54 )55}Pro tip: Recharts FunnelChart does not support a built-in conversion rate label. Use a custom LabelList with a formatter function that appends the percentage so it shows directly on each funnel layer.
Expected result: The funnel chart renders with one colored layer per stage. The layers narrow as leads decrease through the funnel. Hovering shows the stage breakdown tooltip.
Add stage metric cards
Below the funnel chart, render one shadcn/ui Card per stage showing the lead count, total pipeline value, and conversion rate from the previous stage.
1Build a StagMetricCards component at src/components/funnel/StageMetricCards.tsx.23Requirements:4- Read from the same funnel_metrics query (use React Query's useQuery with the same queryKey so it shares the cached result)5- Render a horizontal scrollable row of Cards on mobile, a grid of 3 on md+ screens6- Each card shows:7 - Stage name as CardTitle8 - Lead count in large bold text9 - Pipeline value formatted as currency10 - Conversion badge: if conversion_rate < 20% show a red Badge, 20-50% yellow, 50%+ green11 - A mini sparkline (just a simple 3-bar BarChart with hardcoded last-3-days trend data for now) using Recharts inside the card12- At the top of the card row, show a summary strip: 'X total leads in pipeline · $Y total value'Pro tip: For the conversion color thresholds, check with your sales team what constitutes a healthy conversion rate for each specific stage — a 10% close rate might be excellent for a cold outbound funnel but poor for a trial-to-paid conversion.
Expected result: A row of metric cards appears below the funnel chart. Each card shows the stage metrics with color-coded conversion badges. The summary strip shows totals across all stages.
Build the leads DataTable with stage filtering
Create a DataTable listing all leads with filter controls for stage, source, and date range. Clicking a row opens the lead detail Sheet.
1Build a LeadsTable component at src/components/funnel/LeadsTable.tsx.23Requirements:4- Fetch leads from Supabase with columns: id, name, email, company, source, stage, value, owner_id, created_at5- Use TanStack Table v8 with columns: Name, Company, Stage (Badge), Value (currency), Source (Badge), Date6- Add filter controls above the table:7 - Stage select (all stages + 'All')8 - Source select (organic, paid, referral, outbound, 'All')9 - Date range picker defaulting to last 30 days10- Clicking any row opens a LeadDetailSheet11- LeadDetailSheet shows: name, email, company, current stage selector (Select with all stages), value Input, owner, notes Textarea, next_action Input, next_action_date DatePicker, and a stage history timeline showing all stage_transitions for this lead ordered by transitioned_at desc12- Saving the Sheet calls supabase.from('leads').update(...).eq('id', lead.id) — the Postgres trigger automatically records the stage transitionExpected result: The leads table is filterable by stage and source. Clicking a lead opens the Sheet with editable fields and a stage transition history timeline below the form.
Add period comparison and publish
Add a period comparison toggle that queries funnel_metrics for the previous period and renders a side-by-side comparison bar chart. Then publish the app from the Publish icon.
1Add a period comparison feature to the SalesFunnelChart component.23Requirements:4- Add a 'Compare to previous period' toggle (shadcn/ui Switch) above the funnel chart5- When enabled, fetch two datasets from Supabase:6 1. Current period leads grouped by stage (date range = selected period)7 2. Previous period leads grouped by stage (date range = same duration shifted back)8- Render both datasets in a grouped BarChart with two bars per stage: current (indigo) and previous (gray)9- Show a legend labeling the two series10- Keep the FunnelChart visible for the current period; place the comparison BarChart below it in a collapsible Collapsible component11- Also add a top-right Publish reminder: show a Button 'Publish latest changes' that opens the Lovable publish flow (just navigate to the correct URL or show instructions in a Dialog)Expected result: Toggling the comparison switch reveals a grouped bar chart comparing current and previous period lead counts per stage. The funnel visual remains unchanged above it.
Complete code
1import { useQuery } from '@tanstack/react-query'2import { supabase } from '@/integrations/supabase/client'3import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'4import { Badge } from '@/components/ui/badge'56type FunnelMetric = {7 stage: string8 lead_count: number9 total_value: number10 conversion_rate: number11}1213function conversionVariant(rate: number): 'default' | 'secondary' | 'destructive' {14 if (rate >= 50) return 'default'15 if (rate >= 20) return 'secondary'16 return 'destructive'17}1819export function StageMetricCards() {20 const { data: metrics = [] } = useQuery<FunnelMetric[]>({21 queryKey: ['funnel_metrics'],22 queryFn: async () => {23 const { data, error } = await supabase.from('funnel_metrics').select('*')24 if (error) throw error25 return data26 },27 staleTime: 60_000,28 })2930 const totalLeads = metrics.reduce((s, m) => s + m.lead_count, 0)31 const totalValue = metrics.reduce((s, m) => s + m.total_value, 0)3233 return (34 <div className="space-y-3">35 <p className="text-sm text-muted-foreground">36 {totalLeads} total leads in pipeline · ${totalValue.toLocaleString()} total value37 </p>38 <div className="grid grid-cols-2 gap-3 md:grid-cols-3 lg:grid-cols-6">39 {metrics.map((m) => (40 <Card key={m.stage}>41 <CardHeader className="pb-1 pt-4">42 <CardTitle className="text-xs font-medium text-muted-foreground">{m.stage}</CardTitle>43 </CardHeader>44 <CardContent className="pb-4">45 <p className="text-2xl font-bold">{m.lead_count}</p>46 <p className="text-xs text-muted-foreground mt-0.5">47 ${m.total_value.toLocaleString()}48 </p>49 <Badge50 variant={conversionVariant(m.conversion_rate)}51 className="mt-2 text-xs"52 >53 {m.conversion_rate}% conv.54 </Badge>55 </CardContent>56 </Card>57 ))}58 </div>59 </div>60 )61}Customization ideas
Weighted funnel by deal value
Switch the funnel chart data key from lead_count to total_value so the funnel layer widths reflect pipeline value rather than lead count — useful when deal sizes vary significantly across stages.
Average time in stage
Add a time_in_stage calculation to the funnel_metrics view using the stage_transitions audit trail. Display 'avg. 4.2 days' on each metric card to identify where leads are getting stuck.
Sales rep leaderboard
Add a separate tab showing a leaderboard table with each rep's lead count per stage and total pipeline value. Query the leads table grouped by owner_id and join with the profiles table for names.
Source attribution breakdown
Add a stacked bar chart showing what percentage of leads at each stage came from organic, paid, referral, or outbound sources — helping identify which acquisition channels produce the best-converting leads.
Funnel goal targets
Add a funnel_goals table where managers set target lead counts per stage. Show a progress bar on each metric card indicating actual vs target, with red/green coloring based on attainment.
Lead scoring
Add a score column to the leads table computed by a Postgres function based on company size, source, and engagement. Surface high-score leads at the top of the DataTable with a fire icon Badge.
Common pitfalls
Pitfall: Updating lead stage directly without using the trigger
How to avoid: Always update the leads.stage column via supabase.from('leads').update({ stage: newStage }) and let the Postgres trigger record the transition automatically.
Pitfall: Using FunnelChart without the Recharts Funnel component import
How to avoid: Always nest a Funnel component inside FunnelChart and pass the correct dataKey matching your data array's value property.
Pitfall: Querying stage_transitions without an index on lead_id
How to avoid: Add CREATE INDEX idx_stage_transitions_lead_id ON stage_transitions(lead_id) in your Supabase SQL editor.
Pitfall: Showing conversion rate as count / total_leads across all stages
How to avoid: Compute conversion_rate in the funnel_metrics view as: ROUND(lead_count::numeric / LAG(lead_count) OVER (ORDER BY stage_order) * 100, 1).
Best practices
- Always record stage transitions via a database trigger rather than application code — triggers fire even when data is modified from the Supabase dashboard or migrations.
- Use a Postgres view for funnel_metrics so the aggregation runs on the database, not in React — the view can be indexed and refreshed independently.
- Add a stage_order column to your stages definition so you can ORDER BY it consistently in queries and charts.
- Enable RLS on stage_transitions — users should only see transition history for leads in their own organization.
- Debounce the date range picker by 300ms to avoid firing a Supabase query on every click during range selection.
- Format all currency values with toLocaleString() and a currencyCode option to display the correct symbol and separator for the user's locale.
- Cache the funnel_metrics query with a 60-second staleTime — funnel aggregates do not need real-time freshness and caching avoids redundant database hits.
- Test the audit trail trigger by updating a lead's stage directly in the Supabase Table Editor and verifying a new row appears in stage_transitions.
AI prompts to try
Copy these prompts to build this project faster.
I have a Supabase leads table with a stage column and a stage_transitions audit table. Help me write a Postgres view called funnel_metrics that returns: stage name, lead count, total pipeline value, and conversion rate (percentage of leads from the previous stage that reached this stage). The stages have a defined order: Awareness, Interest, Consideration, Intent, Evaluation, Purchase.
Add a lead import feature to the sales funnel app. Create a shadcn/ui Dialog with a file input that accepts CSV files. The CSV should have columns: name, email, company, source, stage, value. Parse the file in the browser, show a preview table of the first 5 rows, and on confirm, insert all rows into the leads Supabase table. Show progress and error count after import.
In Lovable, build a Kanban-style stage mover component that sits inside the LeadDetailSheet. Show each funnel stage as a horizontal Button with a count Badge. The current stage is highlighted in indigo. Clicking another stage moves the lead to that stage immediately with an optimistic update, then persists to Supabase. Show a toast with 'Moved to [stage]' on success.
Frequently asked questions
Does Recharts include a FunnelChart out of the box?
Yes. Recharts ships with FunnelChart and Funnel components since version 2.1. Ask Lovable to install recharts and import FunnelChart, Funnel, Tooltip, and LabelList — no extra libraries needed.
How do I change the funnel stage names to match my actual sales process?
The stage names are stored as text values in the leads table. Update the seed data or create a funnel_stages table with your stage names and a sort_order column. Ask Lovable to fetch the stages dynamically so the funnel adapts to whatever stage names you define.
Can I track leads from multiple separate pipelines?
Yes. Add a pipeline_id column to the leads table and a pipelines table. Filter all queries by the active pipeline. The funnel_metrics view should include a WHERE pipeline_id = :id clause so metrics are calculated per pipeline.
How do I see which rep is converting the most leads?
Query the leads table grouped by owner_id and join with profiles for names. Build a separate leaderboard card or table showing each rep's lead count at each stage. This is a good follow-up prompt to give Lovable after the core funnel is working.
The funnel chart layers are not narrowing — all stages show the same width. Why?
Recharts FunnelChart uses the dataKey value to determine each layer's width relative to the maximum. If all values are equal (e.g. all 1), the chart looks uniform. Make sure your funnel_metrics view returns real lead counts per stage, not a count of 1 per row.
How do I prevent reps from moving leads backwards in the funnel?
Add a stage_order lookup to the Postgres trigger and raise an exception if to_stage has a lower order than from_stage. Return the error to the client and show it as a toast notification in the Sheet component.
Can RapidDev help me connect this funnel to my CRM data source?
Yes. RapidDev can help you set up a Supabase Edge Function that syncs lead data from HubSpot, Salesforce, or another CRM into your funnel tables so the visualization stays current with your existing tool.
Is there a way to set conversion rate targets per stage?
Add a funnel_targets table with stage and target_conversion_rate columns. In StageMetricCards, fetch the target alongside the actual rate and show a progress bar indicating how close each stage is to its target.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation