# How to Build Lead generation tool with V0

- Tool: v0
- Difficulty: Intermediate
- Compatibility: V0 Premium or higher
- Last updated: April 2026

## TL;DR

Build a multi-step lead capture tool with V0 using Next.js, Supabase, and shadcn/ui. Features conditional form logic, automatic lead scoring, a Kanban pipeline board, and embeddable form pages — all with server-side scoring to keep your rules private. Takes about 1-2 hours.

## Before you start

- A V0 account (Premium or higher recommended)
- A Supabase project (free tier works — connect via V0's Connect panel)
- Your lead qualification criteria (what makes a lead hot, warm, or cold)
- Form fields you want to capture (company size, budget, timeline, etc.)

## Step-by-step guide

### 1. Set up the database schema for forms, leads, and pipeline

Create the Supabase schema with tables for form definitions, lead submissions, pipeline stages, and lead-stage assignments. The forms table stores step definitions and scoring rules as JSONB.

```
// Paste this prompt into V0's AI chat:
// Build a lead generation tool. Create a Supabase schema:
// 1. forms: id (uuid PK), name (text), slug (text UNIQUE), steps (jsonb), scoring_rules (jsonb), redirect_url (text), owner_id (uuid FK to auth.users)
// 2. leads: id (uuid PK), form_id (uuid FK to forms), email (text), name (text), company (text), responses (jsonb), score (int DEFAULT 0), status (text DEFAULT 'new'), source (text), utm_params (jsonb), created_at (timestamptz)
// 3. pipeline_stages: id (uuid PK), name (text), position (int), color (text)
// 4. lead_stage: lead_id (uuid FK to leads), stage_id (uuid FK to pipeline_stages), moved_at (timestamptz), moved_by (uuid FK to auth.users)
// Add RLS policies. Seed pipeline_stages with: New, Contacted, Qualified, Proposal, Won, Lost.
// Generate SQL and TypeScript types.
```

> Pro tip: Store scoring_rules as JSONB in the forms table — this lets you configure different scoring logic per form without changing code.

**Expected result:** Supabase is connected with all tables created, pipeline stages seeded, and RLS policies active.

### 2. Build the multi-step lead capture form

Create the public-facing form page that walks visitors through multiple steps with conditional logic. Each step shows different fields based on previous answers. UTM parameters from the URL are captured automatically.

```
// Paste this prompt into V0's AI chat:
// Create a multi-step lead form at app/f/[slug]/page.tsx.
// Requirements:
// - Fetch the form definition by slug from Supabase (static generation with generateStaticParams)
// - Parse the steps jsonb to render each step's fields dynamically
// - Use shadcn/ui Input, Select, RadioGroup based on field type from the steps config
// - Show a Progress bar at the top showing step X of Y
// - Navigation: 'Next' Button advances, 'Back' Button goes back
// - On final step, show a 'Submit' Button that POSTs to /api/leads with all responses + UTM params from URL searchParams
// - After submit, redirect to the form's redirect_url or show a thank-you Card
// - Capture utm_source, utm_medium, utm_campaign, utm_content from the page URL automatically
// - Make it responsive and mobile-friendly with max-w-lg centered layout
```

**Expected result:** The public form page renders dynamic multi-step forms with progress tracking, field validation, and UTM parameter capture.

### 3. Create the server-side lead scoring engine

Build the API route that receives lead submissions, calculates a score based on the form's scoring rules, assigns a tier (hot/warm/cold), and stores the lead. Scoring logic runs server-side so it never leaks to visitors.

```
import { createClient } from '@supabase/supabase-js'
import { NextRequest, NextResponse } from 'next/server'

const supabase = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!
)

function calculateScore(
  responses: Record<string, string>,
  rules: { field: string; value: string; points: number }[]
): number {
  let score = 0
  for (const rule of rules) {
    if (responses[rule.field] === rule.value) {
      score += rule.points
    }
  }
  return score
}

function getTier(score: number): string {
  if (score >= 80) return 'hot'
  if (score >= 40) return 'warm'
  return 'cold'
}

export async function POST(req: NextRequest) {
  const { form_id, email, name, company, responses, source, utm_params } =
    await req.json()

  const { data: form } = await supabase
    .from('forms')
    .select('scoring_rules')
    .eq('id', form_id)
    .single()

  const score = form?.scoring_rules
    ? calculateScore(responses, form.scoring_rules)
    : 0

  const { data: lead, error } = await supabase
    .from('leads')
    .insert({
      form_id,
      email,
      name,
      company,
      responses,
      score,
      status: getTier(score),
      source: source || 'direct',
      utm_params: utm_params || {},
    })
    .select()
    .single()

  if (error) {
    return NextResponse.json({ error: error.message }, { status: 500 })
  }

  // Assign to 'New' pipeline stage
  const { data: newStage } = await supabase
    .from('pipeline_stages')
    .select('id')
    .eq('name', 'New')
    .single()

  if (newStage) {
    await supabase.from('lead_stage').insert({
      lead_id: lead.id,
      stage_id: newStage.id,
      moved_at: new Date().toISOString(),
    })
  }

  return NextResponse.json({ success: true, lead_id: lead.id })
}
```

**Expected result:** Lead submissions are scored server-side based on configurable rules and automatically assigned to the 'New' pipeline stage.

### 4. Build the Kanban pipeline dashboard

Create the pipeline management page showing leads organized by stage in a Kanban board. Each lead card shows name, company, score badge, and source. Leads can be moved between stages via a Server Action.

```
// Paste this prompt into V0's AI chat:
// Create a pipeline Kanban board at app/leads/page.tsx.
// Requirements:
// - Fetch all pipeline_stages ordered by position, with leads in each stage
// - Display as horizontal columns (one per stage) with the stage name and count as header
// - Each lead shows as a shadcn/ui Card: name, company, email, score Badge (hot=red, warm=yellow, cold=blue), source, created_at
// - Clicking a lead Card opens a Sheet sidebar with full details: all form responses, score breakdown, stage history timeline, and notes
// - Add a Select dropdown on each lead to move it to a different pipeline stage (Server Action updates lead_stage)
// - Add filters at the top: search Input, score tier Select (hot/warm/cold), source Select, date range
// - Show total lead counts and conversion rate (Won / total) in summary Cards above the board
// - Mobile layout: stack columns vertically with collapsible sections
```

> Pro tip: Use V0's Vars tab to store webhook URLs for pushing leads to external CRMs like HubSpot or Salesforce. Add the webhook call in the lead submission API route after scoring.

**Expected result:** The pipeline page shows a Kanban board with leads organized by stage, score badges, and a detail Sheet sidebar.

### 5. Add the form builder and deploy

Create an interface for building and editing lead capture forms with step configuration, field types, and scoring rules. Then deploy to production.

```
// Paste this prompt into V0's AI chat:
// Create a form builder at app/forms/[id]/edit/page.tsx.
// Requirements:
// - Fetch the form by ID, show current steps and fields
// - Each step is a Card with: step title Input, list of fields below
// - Each field has: label Input, type Select (text/email/select/radio/number), options Textarea (for select/radio, comma-separated), required Switch
// - Add Field Button appends a new field to the current step
// - Add Step Button appends a new step
// - Scoring Rules section: for each select/radio field, show a Table where you assign points to each option value
// - Save Button uses a Server Action to update the form's steps and scoring_rules jsonb
// - Preview Button opens the form in a new tab at /f/[slug]
// - Use shadcn/ui Accordion for collapsible steps, Switch for required toggle, and Separator between sections
```

**Expected result:** The form builder lets you configure multi-step forms with field types, scoring rules, and a live preview link. The app is deployed to Vercel.

## Complete code example

File: `app/api/leads/route.ts`

```typescript
import { createClient } from '@supabase/supabase-js'
import { NextRequest, NextResponse } from 'next/server'

const supabase = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!
)

interface ScoringRule {
  field: string
  value: string
  points: number
}

function calculateScore(
  responses: Record<string, string>,
  rules: ScoringRule[]
): number {
  let score = 0
  for (const rule of rules) {
    if (responses[rule.field] === rule.value) {
      score += rule.points
    }
  }
  return score
}

export async function POST(req: NextRequest) {
  const body = await req.json()
  const { form_id, email, name, company, responses, utm_params } = body

  if (!form_id || !email) {
    return NextResponse.json({ error: 'Missing fields' }, { status: 400 })
  }

  const { data: form } = await supabase
    .from('forms')
    .select('scoring_rules')
    .eq('id', form_id)
    .single()

  const score = form?.scoring_rules
    ? calculateScore(responses, form.scoring_rules)
    : 0

  const tier = score >= 80 ? 'hot' : score >= 40 ? 'warm' : 'cold'

  const { data: lead, error } = await supabase
    .from('leads')
    .insert({
      form_id, email, name, company,
      responses, score, status: tier,
      source: utm_params?.utm_source || 'direct',
      utm_params: utm_params || {},
    })
    .select('id')
    .single()

  if (error) {
    return NextResponse.json({ error: error.message }, { status: 500 })
  }

  return NextResponse.json({ success: true, lead_id: lead.id, score, tier })
}
```

## Common mistakes

- **Running the scoring engine on the client side** — Client-side scoring exposes your scoring rules to visitors. They can see which answers score highest and game the system. Fix: Run the scoring logic in the API route (server-side). The client only submits responses; the server fetches rules from the database, calculates the score, and stores the result.
- **Not capturing UTM parameters from the form URL** — Without source attribution, you cannot measure which marketing channels generate the best leads. Paid ads, organic search, and social all look the same. Fix: Extract utm_source, utm_medium, utm_campaign from the page URL searchParams and include them in the lead submission payload. Store them in the utm_params jsonb column.
- **Making the public form page a client component unnecessarily** — Client-rendered form pages are slower to load and worse for SEO. The initial form layout can be server-rendered for instant display. Fix: Use generateStaticParams to statically generate form pages by slug. Only the interactive step navigation and form submission need 'use client' — extract them as a child component.
- **Using NEXT_PUBLIC_ prefix for the service role key** — The service role key bypasses RLS. Exposing it lets anyone read all leads, scores, and pipeline data. Fix: Store SUPABASE_SERVICE_ROLE_KEY in V0's Vars tab without any prefix. Only use it in API routes.

## Best practices

- Run lead scoring server-side to keep your qualification criteria private — never expose scoring rules to visitors
- Use generateStaticParams for public form pages so they load instantly from the CDN edge
- Store scoring rules as JSONB in the forms table so you can configure different scoring per form without code changes
- Capture UTM parameters automatically from the form URL for accurate source attribution
- Use V0's Vars tab to store webhook URLs for CRM integrations — this keeps credentials out of your code
- Use Server Components for the pipeline dashboard and client components only for the interactive Kanban board and form steps
- Add a unique index on (lead_id, stage_id) in lead_stage to prevent duplicate stage assignments
- Use V0's Design Mode (Option+D) to adjust the form styling, Kanban card layout, and Badge colors without spending credits

## Frequently asked questions

### How does the lead scoring work?

Each form has scoring_rules stored as JSONB — an array of {field, value, points} objects. When a lead submits, the API route matches their responses against the rules, sums the points, and assigns a tier: hot (80+), warm (40-79), or cold (below 40). The scoring logic runs server-side so it is never visible to form visitors.

### Can I embed the form on my existing website?

Yes. The form page at /f/[slug] is a standalone page that works in an iframe. Add an iframe tag pointing to your deployed URL with the form slug. UTM parameters pass through the URL for source tracking.

### Do I need a paid V0 plan?

Premium ($20/month) is recommended. The lead generation tool has multiple complex pages (multi-step form, Kanban board, form builder) that require several prompts to build.

### Can I push leads to my existing CRM?

Yes. Add a webhook call in the lead submission API route that sends the lead data to HubSpot, Salesforce, or any CRM with a REST API. Store the webhook URL in V0's Vars tab to keep it out of your code.

### How do I track which marketing channel generates the best leads?

The form automatically captures UTM parameters (utm_source, utm_medium, utm_campaign) from the URL. You can filter leads by source in the pipeline dashboard and compare average scores across channels.

### How do I deploy the lead generation tool?

Click Share in V0, then Publish to Production. The public form pages are statically generated for fast loading. Set SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY in the Vars tab before deploying.

### Can RapidDev help build a custom lead generation tool?

Yes. RapidDev has built over 600 apps including lead capture systems with advanced scoring, CRM integrations, and marketing automation workflows. Book a free consultation to discuss your requirements.

---

Source: https://www.rapidevelopers.com/how-to-build-v0/lead-generation-tool
© RapidDev — https://www.rapidevelopers.com/how-to-build-v0/lead-generation-tool
