Skip to main content
RapidDev - Software Development Agency

How to Build a Lead Generation Tool with Lovable

Build a lead generation tool in Lovable with a Kanban pipeline, landing page builder, lead scoring engine, and embeddable form snippet. Uses Supabase to store leads, landing pages, and activity history. A database function calculates lead scores automatically. A Supabase Edge Function serves embeddable form HTML for external websites.

What you'll build

  • Lead Kanban pipeline with drag-and-drop stage management and score badges
  • Landing page builder with form field configuration and live preview
  • Lead scoring engine via a Supabase database function that updates on each activity
  • Lead DataTable with score sorting, source filtering, and stage filtering
  • Conversion funnel Chart showing lead-to-close rates across pipeline stages
  • Embeddable form snippet via a Supabase Edge Function that returns standalone HTML
  • Lead activity timeline showing every email open, form submission, and stage change
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate16 min read2-3 hoursLovable Pro or higherApril 2026RapidDev Engineering Team
TL;DR

Build a lead generation tool in Lovable with a Kanban pipeline, landing page builder, lead scoring engine, and embeddable form snippet. Uses Supabase to store leads, landing pages, and activity history. A database function calculates lead scores automatically. A Supabase Edge Function serves embeddable form HTML for external websites.

What you're building

A lead generation and management platform that captures leads from custom landing pages, scores them automatically, and tracks their journey through a sales pipeline.

Leads enter the system through embeddable forms hosted on any external website. Each form submission creates a lead record and logs the first activity. A Supabase database function calculates a lead score (0-100) based on activities: form submission (+10), email opened (+5), link clicked (+10), call completed (+20), demo booked (+30). The score updates automatically after each activity insert.

The Kanban board displays leads grouped by pipeline stage with score badges color-coded green/yellow/red. Drag-and-drop moves leads between stages and logs a stage_change activity. The DataTable gives a sortable, filterable view for bulk management. A funnel Chart shows the conversion rate between each pipeline stage, making bottlenecks immediately visible.

Final result

A fully functional lead generation system where leads flow from embeddable landing page forms through a scored Kanban pipeline, with activity tracking, funnel analytics, and an Edge Function serving embeddable form HTML for external sites.

Tech stack

LovableUI scaffolding and Supabase integration
React + TypeScriptKanban board, form builder, and analytics components
Supabase PostgreSQLLeads, landing_pages, and lead_activities tables
Supabase Edge FunctionsEmbeddable form snippet endpoint serving standalone HTML
shadcn/uiCard, DataTable, Chart, Badge, Sheet, Form, Progress, Tabs
Tailwind CSSKanban layout, score color coding, and pipeline styling

Prerequisites

  • Supabase project with Auth enabled
  • Lovable Pro account for Edge Function deployment
  • SUPABASE_URL and SUPABASE_ANON_KEY added to Lovable Cloud tab → Secrets
  • A domain where you plan to embed the form (for testing the embed snippet)

Build steps

1

Create the Supabase schema with lead scoring function

Set up leads, landing_pages, and lead_activities tables. The lead scoring function recalculates a lead's score every time a new activity is inserted, using weighted point values per activity type.

supabase_schema.sql
1-- Run in Supabase SQL Editor
2create table landing_pages (
3 id uuid primary key default gen_random_uuid(),
4 title text not null,
5 slug text unique not null,
6 headline text,
7 subheadline text,
8 fields jsonb default '[{"id":"name","label":"Full Name","type":"text","required":true},{"id":"email","label":"Email","type":"email","required":true}]',
9 owner_id uuid references auth.users(id),
10 is_published boolean default false,
11 created_at timestamptz default now()
12);
13
14create table leads (
15 id uuid primary key default gen_random_uuid(),
16 landing_page_id uuid references landing_pages(id),
17 full_name text not null,
18 email text not null,
19 phone text,
20 company text,
21 source text default 'form',
22 stage text default 'new' check (stage in ('new','contacted','qualified','proposal','negotiation','closed_won','closed_lost')),
23 score int default 10,
24 owner_id uuid references auth.users(id),
25 created_at timestamptz default now(),
26 updated_at timestamptz default now()
27);
28
29create table lead_activities (
30 id uuid primary key default gen_random_uuid(),
31 lead_id uuid references leads(id) on delete cascade,
32 activity_type text not null check (activity_type in ('form_submitted','email_opened','link_clicked','call_completed','demo_booked','stage_changed','note_added')),
33 metadata jsonb default '{}',
34 created_by uuid references auth.users(id),
35 created_at timestamptz default now()
36);
37
38create index idx_leads_stage_score on leads(stage, score desc);
39create index idx_activities_lead_created on lead_activities(lead_id, created_at);
40
41-- Lead scoring function
42create or replace function calculate_lead_score(p_lead_id uuid)
43returns int language sql as $$
44 select least(100,
45 coalesce(sum(
46 case activity_type
47 when 'form_submitted' then 10
48 when 'email_opened' then 5
49 when 'link_clicked' then 10
50 when 'call_completed' then 20
51 when 'demo_booked' then 30
52 when 'stage_changed' then 5
53 else 2
54 end
55 ), 0)
56 )::int
57 from lead_activities
58 where lead_id = p_lead_id
59$$;
60
61create or replace function trigger_update_lead_score()
62returns trigger language plpgsql as $$
63begin
64 update leads set score = calculate_lead_score(NEW.lead_id),
65 updated_at = now()
66 where id = NEW.lead_id;
67 return NEW;
68end;
69$$;
70
71create trigger lead_score_trigger
72after insert on lead_activities
73for each row execute function trigger_update_lead_score();
74
75-- RLS
76alter table landing_pages enable row level security;
77alter table leads enable row level security;
78alter table lead_activities enable row level security;
79
80create policy "Owners manage pages" on landing_pages for all using (owner_id = auth.uid());
81create policy "Public read published pages" on landing_pages for select using (is_published = true);
82create policy "Owners manage leads" on leads for all using (owner_id = auth.uid());
83create policy "Public insert leads" on leads for insert with check (true);
84create policy "Owners manage activities" on lead_activities for all using (
85 lead_id in (select id from leads where owner_id = auth.uid())
86);
87create policy "System insert activities" on lead_activities for insert with check (true);

Pro tip: The least(100, ...) cap in calculate_lead_score prevents scores from exceeding 100 no matter how many activities a lead accumulates. This keeps the score scale predictable for visual color coding.

Expected result: Three tables created. The calculate_lead_score function and trigger appear in Supabase under Database → Functions. Every new lead_activities row now automatically updates the parent lead's score.

2

Build the lead Kanban pipeline

Prompt Lovable to create the main pipeline view with stage columns and lead cards showing name, company, score badge, and source. Dragging a card between columns logs a stage_changed activity.

prompt.txt
1Build a /pipeline page showing leads as a Kanban board.
2
3Stage columns (in order): New, Contacted, Qualified, Proposal, Negotiation, Closed Won, Closed Lost
4Fetch leads from Supabase grouped by stage.
5
6Render each stage as a vertical column:
7- Column header: stage name + lead count Badge
8- Total potential value (count) in smaller text below
9
10Each lead card (shadcn Card) shows:
11- full_name (bold)
12- company (small text, gray)
13- source Badge (form=blue, import=gray, referral=green, manual=orange)
14- score Badge: score >= 70 = green, 40-69 = yellow, < 40 = red
15- Updated_at relative time
16
17Drag-and-drop between columns:
181. Update leads.stage in Supabase
192. Insert a lead_activities row: activity_type='stage_changed', metadata: {from_stage, to_stage}
203. Use optimistic updates update UI immediately, sync in background
21
22Clicking a card navigates to /leads/:id (detail page).
23Add a floating 'Add Lead' Button that opens a Sheet form.

Expected result: The Kanban shows leads grouped by stage. Dragging a card to a new column updates the stage in Supabase and inserts a stage_changed activity. The score badge color updates reflect the lead's current score.

3

Build the lead detail page with activity timeline

The lead detail page shows full contact info, score, stage selector, and a chronological activity timeline. Logging a new activity from this page triggers the scoring function.

src/pages/LeadDetailPage.tsx
1import { useState, useEffect } from 'react'
2import { useParams } from 'react-router-dom'
3import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
4import { Badge } from '@/components/ui/badge'
5import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
6import { Button } from '@/components/ui/button'
7import { Progress } from '@/components/ui/progress'
8import { supabase } from '@/lib/supabase'
9import { toast } from 'sonner'
10import { formatDistanceToNow } from 'date-fns'
11
12const STAGES = ['new','contacted','qualified','proposal','negotiation','closed_won','closed_lost']
13const ACTIVITY_POINTS: Record<string, number> = {
14 form_submitted: 10, email_opened: 5, link_clicked: 10,
15 call_completed: 20, demo_booked: 30, stage_changed: 5, note_added: 2
16}
17
18const ACTIVITY_ICONS: Record<string, string> = {
19 form_submitted: '📋', email_opened: '✉️', link_clicked: '🔗',
20 call_completed: '📞', demo_booked: '🗓️', stage_changed: '🔄', note_added: '📝'
21}
22
23interface Lead {
24 id: string; full_name: string; email: string; phone: string | null
25 company: string | null; source: string; stage: string; score: number
26 created_at: string
27}
28
29interface Activity {
30 id: string; activity_type: string; metadata: Record<string, string>
31 created_at: string; created_by: string | null
32}
33
34export function LeadDetailPage() {
35 const { id } = useParams<{ id: string }>()
36 const [lead, setLead] = useState<Lead | null>(null)
37 const [activities, setActivities] = useState<Activity[]>([])
38
39 useEffect(() => {
40 if (!id) return
41 Promise.all([
42 supabase.from('leads').select('*').eq('id', id).single(),
43 supabase.from('lead_activities').select('*').eq('lead_id', id).order('created_at', { ascending: false })
44 ]).then(([{ data: l }, { data: a }]) => {
45 if (l) setLead(l)
46 if (a) setActivities(a)
47 })
48 }, [id])
49
50 const logActivity = async (type: string) => {
51 if (!id) return
52 const { error } = await supabase.from('lead_activities').insert({ lead_id: id, activity_type: type })
53 if (error) { toast.error('Failed to log activity'); return }
54 toast.success(`Logged: ${type.replace('_', ' ')}`)
55 const { data } = await supabase.from('leads').select('score').eq('id', id).single()
56 if (data) setLead(prev => prev ? { ...prev, score: data.score } : null)
57 }
58
59 const updateStage = async (stage: string) => {
60 if (!id || !lead) return
61 await supabase.from('leads').update({ stage }).eq('id', id)
62 await supabase.from('lead_activities').insert({
63 lead_id: id, activity_type: 'stage_changed',
64 metadata: { from_stage: lead.stage, to_stage: stage }
65 })
66 setLead(prev => prev ? { ...prev, stage } : null)
67 }
68
69 const scoreColor = (s: number) => s >= 70 ? 'text-green-600' : s >= 40 ? 'text-yellow-600' : 'text-red-600'
70
71 if (!lead) return <div className="p-8">Loading...</div>
72
73 return (
74 <div className="max-w-4xl mx-auto p-8 space-y-6">
75 <div className="flex justify-between items-start">
76 <div>
77 <h1 className="text-2xl font-bold">{lead.full_name}</h1>
78 <p className="text-muted-foreground">{lead.email} {lead.phone && `· ${lead.phone}`}</p>
79 {lead.company && <p className="text-sm">{lead.company}</p>}
80 </div>
81 <div className="text-right">
82 <p className={`text-3xl font-bold ${scoreColor(lead.score)}`}>{lead.score}</p>
83 <p className="text-xs text-muted-foreground">Lead Score</p>
84 <Progress value={lead.score} className="mt-1 w-24" />
85 </div>
86 </div>
87 <div className="flex gap-3 items-center">
88 <Select value={lead.stage} onValueChange={updateStage}>
89 <SelectTrigger className="w-48"><SelectValue /></SelectTrigger>
90 <SelectContent>{STAGES.map(s => <SelectItem key={s} value={s}>{s.replace('_', ' ')}</SelectItem>)}</SelectContent>
91 </Select>
92 {['email_opened','call_completed','demo_booked','note_added'].map(t => (
93 <Button key={t} variant="outline" size="sm" onClick={() => logActivity(t)}>
94 {ACTIVITY_ICONS[t]} {t.replace('_', ' ')}
95 </Button>
96 ))}
97 </div>
98 <div className="space-y-3">
99 <h2 className="font-semibold">Activity Timeline</h2>
100 {activities.map(a => (
101 <div key={a.id} className="flex gap-3 items-start">
102 <span className="text-lg">{ACTIVITY_ICONS[a.activity_type]}</span>
103 <div>
104 <p className="text-sm font-medium">{a.activity_type.replace(/_/g, ' ')}
105 <span className="text-green-600 ml-2">+{ACTIVITY_POINTS[a.activity_type] ?? 2} pts</span>
106 </p>
107 <p className="text-xs text-muted-foreground">{formatDistanceToNow(new Date(a.created_at))} ago</p>
108 </div>
109 </div>
110 ))}
111 </div>
112 </div>
113 )
114}

Pro tip: After logging an activity, re-fetch the lead's score from Supabase rather than calculating it in JavaScript. The database trigger already computed the correct value — fetching it keeps the UI in sync with the database state.

Expected result: The lead detail page shows full contact info, a score progress bar, activity log buttons, and a full activity timeline. Clicking 'call_completed' adds the activity and updates the score badge immediately.

4

Build the embeddable form Edge Function

A Supabase Edge Function responds to GET /embed/:slug and returns a self-contained HTML page with a form that posts submissions back to the Supabase API. This lets external sites embed the form with a single iframe tag.

supabase/functions/embed-form/index.ts
1// supabase/functions/embed-form/index.ts
2import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
3import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
4
5const corsHeaders = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'content-type' }
6
7serve(async (req) => {
8 if (req.method === 'OPTIONS') return new Response('ok', { headers: corsHeaders })
9
10 const url = new URL(req.url)
11 const slug = url.pathname.split('/').pop()
12 if (!slug) return new Response('Missing slug', { status: 400 })
13
14 const supabase = createClient(Deno.env.get('SUPABASE_URL')!, Deno.env.get('SUPABASE_ANON_KEY')!)
15 const { data: page } = await supabase.from('landing_pages').select('*').eq('slug', slug).eq('is_published', true).single()
16
17 if (!page) return new Response('Form not found', { status: 404 })
18
19 const fields = (page.fields as Array<{ id: string; label: string; type: string; required: boolean }>) ?? []
20 const fieldsHtml = fields.map(f => `
21 <div style="margin-bottom:16px">
22 <label style="display:block;font-size:14px;font-weight:500;margin-bottom:4px">${f.label}${f.required ? ' *' : ''}</label>
23 <input name="${f.id}" type="${f.type}" ${f.required ? 'required' : ''}
24 style="width:100%;padding:8px 12px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px;box-sizing:border-box" />
25 </div>`).join('')
26
27 const html = `<!DOCTYPE html>
28<html><head><meta charset="utf-8"><style>body{font-family:system-ui,sans-serif;margin:0;padding:24px;background:#fff}h1{font-size:22px;margin-bottom:8px}p{color:#666;font-size:14px;margin-bottom:24px}button{background:#2563eb;color:#fff;border:none;padding:10px 24px;border-radius:6px;cursor:pointer;font-size:14px}button:hover{background:#1d4ed8}#success{display:none;color:#16a34a;font-weight:500}</style></head>
29<body>
30 <h1>${page.headline ?? page.title}</h1>
31 ${page.subheadline ? `<p>${page.subheadline}</p>` : ''}
32 <form id="lf">${fieldsHtml}<button type="submit">Submit</button></form>
33 <p id="success">Thank you! We will be in touch.</p>
34 <script>
35 document.getElementById('lf').onsubmit = async function(e) {
36 e.preventDefault();
37 const fd = new FormData(e.target);
38 const data = {}; fd.forEach((v,k)=>data[k]=v);
39 await fetch('${Deno.env.get('SUPABASE_URL')}/rest/v1/leads', {
40 method: 'POST',
41 headers: {'Content-Type':'application/json','apikey':'${Deno.env.get('SUPABASE_ANON_KEY')}','Prefer':'return=minimal'},
42 body: JSON.stringify({full_name:data.name||data.full_name||'',email:data.email||'',landing_page_id:'${page.id}',source:'embed'})
43 });
44 e.target.style.display='none';
45 document.getElementById('success').style.display='block';
46 }
47 </script>
48</body></html>`
49
50 return new Response(html, { headers: { ...corsHeaders, 'Content-Type': 'text/html' } })
51})

Pro tip: After deploying, add a 'Get Embed Code' Button in the landing page builder that shows: <iframe src='[EDGE_FUNCTION_URL]/embed-form/[slug]' width='100%' height='400' frameborder='0'></iframe>. Include a copy-to-clipboard action.

Expected result: The Edge Function URL returns a fully styled standalone HTML form. Pasting the iframe snippet into any external website shows the form and submits leads to Supabase.

5

Build the funnel Chart and lead DataTable

The analytics page shows a conversion funnel chart and a leads DataTable with score sorting and stage filtering. The funnel shows how many leads are in each stage and the conversion rate between consecutive stages.

prompt.txt
1Build an /analytics page with two sections:
2
31. Conversion Funnel Chart:
4 Fetch lead counts grouped by stage from Supabase.
5 Stage order: new contacted qualified proposal negotiation closed_won
6 Render a vertical funnel using a shadcn BarChart (recharts) in horizontal mode:
7 - Each bar is a stage, bar width proportional to lead count
8 - Show conversion % between bars: 'contacted / new = X%'
9 - Color: green for closed_won, red for closed_lost, blue for all others
10 - Show closed_lost as a separate donut chart below the funnel
11
122. Leads DataTable at /leads:
13 Fetch all leads with columns:
14 - full_name (link to /leads/:id)
15 - email
16 - company
17 - source Badge
18 - stage Badge (color per stage)
19 - score (sortable, with color: green >= 70, yellow >= 40, red < 40)
20 - created_at (formatted)
21 - Actions: DropdownMenu with 'View', 'Move to Qualified', 'Mark Closed Won', 'Mark Closed Lost'
22
23 Above table filters:
24 - Stage Select filter
25 - Source Select filter
26 - Score range: Slider with min/max handles (0 to 100)
27 - Search Input for name or email
28
29 Sort by score descending by default.

Expected result: The analytics page shows a horizontal funnel chart with per-stage counts and conversion percentages. The DataTable sorts leads by score with color-coded badges and all filter combinations work correctly.

Complete code

src/hooks/useLeadPipeline.ts
1import { useState, useEffect, useCallback } from 'react'
2import { supabase } from '@/lib/supabase'
3import { toast } from 'sonner'
4
5export const PIPELINE_STAGES = ['new', 'contacted', 'qualified', 'proposal', 'negotiation', 'closed_won', 'closed_lost'] as const
6export type PipelineStage = typeof PIPELINE_STAGES[number]
7
8export interface Lead {
9 id: string
10 full_name: string
11 email: string
12 company: string | null
13 source: string
14 stage: PipelineStage
15 score: number
16 updated_at: string
17}
18
19export function useLeadPipeline() {
20 const [leads, setLeads] = useState<Record<PipelineStage, Lead[]>>(
21 Object.fromEntries(PIPELINE_STAGES.map(s => [s, []])) as Record<PipelineStage, Lead[]>
22 )
23 const [loading, setLoading] = useState(true)
24
25 const load = useCallback(async () => {
26 setLoading(true)
27 const { data, error } = await supabase
28 .from('leads')
29 .select('id, full_name, email, company, source, stage, score, updated_at')
30 .order('score', { ascending: false })
31 if (error) { toast.error('Failed to load leads'); setLoading(false); return }
32 const grouped = Object.fromEntries(PIPELINE_STAGES.map(s => [s, []])) as Record<PipelineStage, Lead[]>
33 data?.forEach(l => { if (grouped[l.stage as PipelineStage]) grouped[l.stage as PipelineStage].push(l as Lead) })
34 setLeads(grouped)
35 setLoading(false)
36 }, [])
37
38 const moveLead = async (leadId: string, fromStage: PipelineStage, toStage: PipelineStage) => {
39 setLeads(prev => {
40 const next = { ...prev }
41 const idx = next[fromStage].findIndex(l => l.id === leadId)
42 if (idx === -1) return prev
43 const [lead] = next[fromStage].splice(idx, 1)
44 next[toStage] = [...next[toStage], { ...lead, stage: toStage }]
45 return next
46 })
47 await supabase.from('leads').update({ stage: toStage }).eq('id', leadId)
48 await supabase.from('lead_activities').insert({
49 lead_id: leadId, activity_type: 'stage_changed',
50 metadata: { from_stage: fromStage, to_stage: toStage }
51 })
52 }
53
54 useEffect(() => { load() }, [load])
55 return { leads, loading, moveLead, reload: load }
56}

Customization ideas

Email sequence automation

When a lead enters the Contacted stage, automatically trigger a Supabase Edge Function that sends a sequence of emails via Resend. Log each send as an email_opened activity when the tracking pixel fires.

Lead source attribution

Pass UTM parameters (utm_source, utm_medium, utm_campaign) from the embed form URL to the leads table. Add source breakdown charts on the analytics page to show which channels generate the highest-scored leads.

Revenue tracking

Add a deal_value column to leads. Show total pipeline value per stage on the Kanban columns and a projected revenue chart on the analytics page based on win probability per stage.

Lead assignment and team routing

Add an assigned_to column to leads. When a new lead is created, auto-assign it to the team member with the fewest open leads using a round-robin Supabase function.

CSV import for bulk lead upload

Add a CSV import Dialog that accepts a spreadsheet of leads with name, email, company, and source columns. Parse client-side and bulk-insert to Supabase, then log a form_submitted activity for each.

Common pitfalls

Pitfall: Calculating lead scores in React instead of using the database trigger

How to avoid: Always use the Postgres trigger set up in Step 1. Every lead_activities insert automatically recalculates and saves the score back to leads.score.

Pitfall: Not logging stage_changed activities when moving leads on the Kanban

How to avoid: Always insert a lead_activities row with activity_type='stage_changed' whenever leads.stage is updated, as shown in the useLeadPipeline hook.

Pitfall: Exposing the Supabase service role key in the embed-form Edge Function HTML

How to avoid: Only use the anon key in the embed HTML. The insert policy on leads is intentionally permissive (with check (true)) so the anon key can insert without bypassing RLS.

Pitfall: Not scoping landing page visibility with is_published

How to avoid: The Edge Function checks .eq('is_published', true) before returning the form HTML. Un-published pages return a 404.

Best practices

  • Always calculate lead scores in the database via triggers — never in JavaScript where results are not persisted
  • Log every meaningful lead interaction as a lead_activities row so the timeline is complete and scores are accurate
  • Use optimistic updates on the Kanban board so drag-and-drop feels instant even on slow connections
  • Key embed forms by a readable slug (not UUID) so form URLs are shareable and memorable
  • Cap lead scores at 100 using least(100, ...) to keep the color-coding scale predictable
  • Sort the lead DataTable by score descending by default so the hottest leads are always at the top
  • Enable Supabase Realtime on leads so the Kanban updates when a team member moves a lead without requiring a page refresh
  • Use Lovable Plan Mode to design the scoring logic and Edge Function before generating code — both components have tricky edge cases worth planning first

AI prompts to try

Copy these prompts to build this project faster.

ChatGPT Prompt

Write a PostgreSQL function called calculate_lead_score that sums weighted activity points from a lead_activities table (form_submitted=10, email_opened=5, link_clicked=10, call_completed=20, demo_booked=30) and caps the result at 100. Include a trigger that calls this function after every INSERT on lead_activities.

Lovable Prompt

Build a lead generation tool with a /pipeline Kanban board showing leads grouped by stage with score badges, an /analytics funnel Chart showing conversion rates between stages, and a /leads DataTable sortable by score with stage and source filters.

Build Prompt

Create a Supabase Edge Function called embed-form that accepts a GET request with a slug parameter, fetches the matching published landing page from Supabase, and returns a standalone HTML page with a form that submits leads to Supabase via the REST API. The form fields should be generated dynamically from the landing page's fields JSONB column.

Frequently asked questions

How does the lead score get calculated?

The calculate_lead_score Postgres function sums up points from the lead_activities table: form_submitted=10, email_opened=5, link_clicked=10, call_completed=20, demo_booked=30, stage_changed=5. A database trigger runs this function automatically after every INSERT on lead_activities and writes the result back to leads.score, capped at 100.

Can I customize the scoring weights?

Yes. Edit the CASE statement inside calculate_lead_score in the Supabase SQL editor and click Save. The updated weights will apply to all future activity inserts. Existing scores will not retroactively update — to recalculate all existing scores, run: UPDATE leads SET score = calculate_lead_score(id).

How does the embeddable form work on external sites?

The embed-form Edge Function serves a self-contained HTML page that posts directly to the Supabase REST API using the anon key. Any website can embed it with an iframe tag. The anon key is safe to use here because the RLS policy on leads allows inserts but not reads for unauthenticated requests.

Can I build this on the Lovable free plan?

The Kanban board, DataTable, and basic scoring work on the free plan. The embed-form Edge Function requires Lovable Pro. The free plan's 5 daily credits make iterating on a multi-feature build like this difficult — Pro is strongly recommended for faster iteration.

How do I deploy this with a custom domain for the landing pages?

Click the Publish icon in Lovable (top-right) and follow the Settings → Custom Domain instructions. For landing pages, the public /forms/:slug route works without authentication. Add your custom domain to Supabase Auth → URL Configuration → Redirect URLs so auth flows work after the domain change.

How do I prevent spam leads from the embeddable form?

Add a honeypot hidden field to the embed form HTML (a field named 'website' styled display:none). In the Edge Function's insert logic, reject submissions where the honeypot field is non-empty. For stronger protection, integrate Cloudflare Turnstile (free tier) and verify the token server-side before inserting the lead.

My Kanban has too many leads to display. How do I add pagination?

Add a limit to each stage's Supabase query (.limit(50)) and a 'Load more' Button at the bottom of each column that fetches the next page with .range(offset, offset+49). Store the offset per stage in component state. Alternatively, add a virtual scrolling library for very long columns.

Where can I get help if my scoring logic or embed form isn't working?

Use Lovable's Plan Mode to describe the exact problem — Plan Mode never modifies your code, so it is safe for debugging sessions. For Supabase function issues, test the trigger directly in the SQL editor with: INSERT INTO lead_activities (lead_id, activity_type) VALUES ('your-lead-uuid', 'call_completed'); then SELECT score FROM leads WHERE id = 'your-lead-uuid'. RapidDev can review your setup if you are still stuck.

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.