Skip to main content
RapidDev - Software Development Agency

How to Build a Sales Funnel App with Lovable

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'll build

  • Supabase schema with leads and stage_transitions tables and RLS policies
  • Funnel chart built with Recharts FunnelChart showing drop-off at each stage
  • Metric Cards for each stage showing count, total value, and conversion rate
  • Stage transition audit trail recording every lead movement with timestamps
  • Lead management DataTable with filters by stage, source, and owner
  • Lead detail Sheet panel with full history, notes, and next-action fields
  • Summary bar comparing current period funnel to the previous period
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate12 min read2–3 hoursLovable Pro or higherApril 2026RapidDev Engineering Team
TL;DR

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

LovableFrontend
SupabaseDatabase & Auth
RechartsFunnel and Bar Charts
shadcn/uiUI Components
TanStack Table v8Leads DataTable
date-fnsDate Formatting

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

1

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.

prompt.txt
1Create a sales funnel app with Supabase. Set up these tables:
2
3- 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_at
4
5- stage_transitions: id, lead_id, from_stage (text, nullable), to_stage (text), transitioned_by (uuid references auth.users), transitioned_at (timestamptz default now())
6
7Create 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.
8
9Enable 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).
10
11Seed 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.

2

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.

src/components/funnel/SalesFunnelChart.tsx
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'
5
6type FunnelMetric = {
7 stage: string
8 lead_count: number
9 total_value: number
10 conversion_rate: number
11}
12
13const STAGE_COLORS = ['#6366f1', '#8b5cf6', '#a78bfa', '#c4b5fd', '#ddd6fe', '#ede9fe']
14
15export 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 error
21 return data
22 },
23 })
24
25 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 }))
32
33 if (isLoading) return <div className="h-64 animate-pulse bg-muted rounded-lg" />
34
35 return (
36 <Card>
37 <CardHeader><CardTitle>Sales Funnel</CardTitle></CardHeader>
38 <CardContent>
39 <ResponsiveContainer width="100%" height={360}>
40 <FunnelChart>
41 <Tooltip
42 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.

3

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.

prompt.txt
1Build a StagMetricCards component at src/components/funnel/StageMetricCards.tsx.
2
3Requirements:
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+ screens
6- Each card shows:
7 - Stage name as CardTitle
8 - Lead count in large bold text
9 - Pipeline value formatted as currency
10 - Conversion badge: if conversion_rate < 20% show a red Badge, 20-50% yellow, 50%+ green
11 - A mini sparkline (just a simple 3-bar BarChart with hardcoded last-3-days trend data for now) using Recharts inside the card
12- 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.

4

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.

prompt.txt
1Build a LeadsTable component at src/components/funnel/LeadsTable.tsx.
2
3Requirements:
4- Fetch leads from Supabase with columns: id, name, email, company, source, stage, value, owner_id, created_at
5- Use TanStack Table v8 with columns: Name, Company, Stage (Badge), Value (currency), Source (Badge), Date
6- 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 days
10- Clicking any row opens a LeadDetailSheet
11- 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 desc
12- Saving the Sheet calls supabase.from('leads').update(...).eq('id', lead.id) the Postgres trigger automatically records the stage transition

Expected 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.

5

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.

prompt.txt
1Add a period comparison feature to the SalesFunnelChart component.
2
3Requirements:
4- Add a 'Compare to previous period' toggle (shadcn/ui Switch) above the funnel chart
5- 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 series
10- Keep the FunnelChart visible for the current period; place the comparison BarChart below it in a collapsible Collapsible component
11- 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

src/components/funnel/StageMetricCards.tsx
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'
5
6type FunnelMetric = {
7 stage: string
8 lead_count: number
9 total_value: number
10 conversion_rate: number
11}
12
13function conversionVariant(rate: number): 'default' | 'secondary' | 'destructive' {
14 if (rate >= 50) return 'default'
15 if (rate >= 20) return 'secondary'
16 return 'destructive'
17}
18
19export 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 error
25 return data
26 },
27 staleTime: 60_000,
28 })
29
30 const totalLeads = metrics.reduce((s, m) => s + m.lead_count, 0)
31 const totalValue = metrics.reduce((s, m) => s + m.total_value, 0)
32
33 return (
34 <div className="space-y-3">
35 <p className="text-sm text-muted-foreground">
36 {totalLeads} total leads in pipeline · ${totalValue.toLocaleString()} total value
37 </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 <Badge
50 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.

ChatGPT Prompt

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.

Lovable Prompt

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.

Build Prompt

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.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help building your app?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.