Build a pipeline-based CRM system with V0 using Next.js and Supabase. You'll get a Kanban deal board with intuitive drag-and-drop, contact management, activity timeline, weighted revenue forecasting, and global search — all in about 1-2 hours without any local setup.
What you're building
Every sales team needs a CRM to track contacts, manage deals through pipeline stages, and forecast revenue. But most CRM software is bloated, expensive, and designed for enterprise teams. Founders and small teams need something simple that just works.
V0 generates the complete CRM interface from prompts — Kanban boards, contact tables, deal detail pages, and activity timelines. Supabase via the Connect panel provides the database with RLS for multi-user isolation and real-time updates for collaborative use.
The architecture uses Next.js Server Components for contact lists and deal pages (fast server-rendered data fetching), a Client Component for the Kanban board with @hello-pangea/dnd for drag-and-drop, Server Actions for all mutations (deal stage updates, contact creation, activity logging), and Supabase with RLS policies scoped by owner_id for multi-tenant isolation.
Final result
A complete CRM with a Kanban deal board, contact management, activity timeline, pipeline revenue forecasting, and global search — all behind Supabase Auth with multi-user data isolation.
Tech stack
Prerequisites
- A V0 account (Premium plan recommended for multi-page builds)
- A Supabase project (free tier works — connect via V0's Connect panel)
- Understanding of your sales pipeline stages (e.g., Lead, Qualified, Proposal, Won, Lost)
Build steps
Set up the CRM database schema with pipeline stages
Create a new V0 project, connect Supabase via the Connect panel, and create the contacts, pipelines, deals, and activities tables. The pipeline stages are stored as a JSONB column for easy customization.
1// Paste this prompt into V0's AI chat:2// Build a CRM system with Supabase. Create these tables:3// 1. contacts: id (uuid PK), owner_id (uuid FK to auth.users), company (text), name (text), email (text), phone (text), source (text), tags (text[]), created_at (timestamptz)4// 2. pipelines: id (uuid PK), org_id (uuid FK), name (text), stages (jsonb DEFAULT '[{"name":"Lead","order":0},{"name":"Qualified","order":1},{"name":"Proposal","order":2},{"name":"Won","order":3},{"name":"Lost","order":4}]')5// 3. deals: id (uuid PK), contact_id (uuid FK), pipeline_id (uuid FK), stage (text), title (text), value (numeric), expected_close (date), owner_id (uuid FK), created_at (timestamptz), updated_at (timestamptz)6// 4. activities: id (uuid PK), deal_id (uuid FK), user_id (uuid FK), type (text CHECK in 'call','email','meeting','note'), content (text), created_at (timestamptz)7// Add RLS policies scoped by owner_id matching auth.uid().Pro tip: Use the Connect panel to add Supabase in two clicks — it auto-provisions NEXT_PUBLIC_SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY in the Vars tab. Then use V0 to generate the SQL migration.
Expected result: Supabase is connected with CRM tables created, pipeline stages configured as JSONB, and RLS policies restricting data by owner.
Build the Kanban deal board with drag-and-drop
Create the main deals page with a Kanban board layout. Each pipeline stage is a column, and deal Cards can be dragged between columns. Stage changes update the deal in Supabase via a Server Action with optimistic UI.
1// Paste this prompt into V0's AI chat:2// Build a Kanban deal board at app/deals/page.tsx.3// Requirements:4// - 'use client' component using @hello-pangea/dnd for drag-and-drop5// - Columns for each pipeline stage (Lead, Qualified, Proposal, Won, Lost)6// - Deal Cards in each column showing: title, value (formatted as currency), contact name, expected close date, owner Avatar7// - Drag a Card between columns to change its stage8// - On drop, optimistically update the local state and call a Server Action to update deals.stage in Supabase9// - If the Server Action fails, revert the optimistic state and show an error toast10// - Column headers show the count of deals and total value11// - Add a "+" Button on each column header to create a new deal in that stage12// - Use shadcn/ui Card for deal tiles, Badge for stage labels, Avatar for owners13// - Add Tabs to switch between Kanban and list Table viewsExpected result: The deal board shows pipeline stages as columns with draggable deal Cards. Dropping a Card in a new column updates the stage optimistically.
Create the contact management page with search and filtering
Build the contacts page with a searchable, filterable Table. Users can add new contacts via a Sheet slide-over, view contact details, and filter by tags and source.
1import { createClient } from '@supabase/supabase-js'2import { ContactTable } from '@/components/contact-table'3import { Button } from '@/components/ui/button'4import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'56const supabase = createClient(7 process.env.SUPABASE_URL!,8 process.env.SUPABASE_SERVICE_ROLE_KEY!9)1011export default async function ContactsPage() {12 const { data: contacts } = await supabase13 .from('contacts')14 .select('*, deals(id, title, value, stage)')15 .order('created_at', { ascending: false })1617 const totalContacts = contacts?.length ?? 018 const withDeals = contacts?.filter((c) => c.deals?.length > 0).length ?? 01920 return (21 <div className="space-y-6">22 <div className="flex items-center justify-between">23 <h1 className="text-3xl font-bold">Contacts</h1>24 <Button>Add Contact</Button>25 </div>26 <div className="grid gap-4 md:grid-cols-3">27 <Card>28 <CardHeader><CardTitle>Total Contacts</CardTitle></CardHeader>29 <CardContent className="text-3xl font-bold">{totalContacts}</CardContent>30 </Card>31 <Card>32 <CardHeader><CardTitle>With Active Deals</CardTitle></CardHeader>33 <CardContent className="text-3xl font-bold">{withDeals}</CardContent>34 </Card>35 </div>36 <ContactTable contacts={contacts ?? []} />37 </div>38 )39}Pro tip: Use Design Mode (Option+D) to adjust the Table column widths, contact Card spacing, and Badge colors for tags without spending any credits.
Expected result: The contacts page shows a searchable Table with stats Cards. The Sheet slide-over allows adding new contacts without navigating away.
Build the deal detail page with activity timeline
Create the deal detail page showing all deal information and a chronological activity timeline. Sales reps can log calls, emails, meetings, and notes. Activities are displayed in a vertical timeline with type-specific icons.
1'use server'23import { createClient } from '@supabase/supabase-js'4import { revalidatePath } from 'next/cache'56const supabase = createClient(7 process.env.SUPABASE_URL!,8 process.env.SUPABASE_SERVICE_ROLE_KEY!9)1011export async function updateDealStage(dealId: string, stage: string) {12 const { error } = await supabase13 .from('deals')14 .update({ stage, updated_at: new Date().toISOString() })15 .eq('id', dealId)1617 if (error) throw new Error(error.message)18 revalidatePath('/deals')19}2021export async function logActivity(22 dealId: string,23 userId: string,24 type: 'call' | 'email' | 'meeting' | 'note',25 content: string26) {27 const { error } = await supabase.from('activities').insert({28 deal_id: dealId,29 user_id: userId,30 type,31 content,32 })3334 if (error) throw new Error(error.message)35 revalidatePath(`/deals/${dealId}`)36}3738export async function createDeal(39 contactId: string,40 pipelineId: string,41 title: string,42 value: number,43 stage: string,44 ownerId: string45) {46 const { error } = await supabase.from('deals').insert({47 contact_id: contactId,48 pipeline_id: pipelineId,49 title,50 value,51 stage,52 owner_id: ownerId,53 })5455 if (error) throw new Error(error.message)56 revalidatePath('/deals')57}Expected result: The deal detail page shows deal info, contact details, and a timeline of activities. Users can log new activities with type selection and content.
Add pipeline revenue forecasting and global search
Build a forecasting view that calculates expected revenue based on deal values and stage probability weights. Add a global search using shadcn/ui Command palette to search across contacts and deals.
1// Paste this prompt into V0's AI chat:2// Build two features:3// 1. Revenue forecast at app/forecast/page.tsx:4// - Assign probability weights to pipeline stages: Lead=10%, Qualified=30%, Proposal=60%, Won=100%, Lost=0%5// - Calculate weighted pipeline value: SUM(deal.value * stage_probability)6// - Show summary Cards: Total Pipeline Value, Weighted Forecast, Deals Closing This Month7// - BarChart (Recharts) showing deal values by stage8// - Table of deals sorted by expected_close date with stage Badge and weighted value9// 2. Global search using shadcn/ui Command (Cmd+K):10// - Search across contacts (by name, company, email) and deals (by title)11// - Show results grouped by type with icons12// - Navigate to contact or deal detail page on selection13// - Use Supabase full-text search with to_tsvector on searchable columnsExpected result: The forecast page shows weighted pipeline revenue with charts. Cmd+K opens a global search across contacts and deals.
Complete code
1'use server'23import { createClient } from '@supabase/supabase-js'4import { revalidatePath } from 'next/cache'56const supabase = createClient(7 process.env.SUPABASE_URL!,8 process.env.SUPABASE_SERVICE_ROLE_KEY!9)1011export async function updateDealStage(dealId: string, stage: string) {12 const { error } = await supabase13 .from('deals')14 .update({ stage, updated_at: new Date().toISOString() })15 .eq('id', dealId)16 if (error) throw new Error(error.message)17 revalidatePath('/deals')18}1920export async function logActivity(21 dealId: string,22 userId: string,23 type: 'call' | 'email' | 'meeting' | 'note',24 content: string25) {26 const { error } = await supabase.from('activities').insert({27 deal_id: dealId,28 user_id: userId,29 type,30 content,31 })32 if (error) throw new Error(error.message)33 revalidatePath(`/deals/${dealId}`)34}3536export async function createContact(37 ownerId: string,38 name: string,39 company: string,40 email: string,41 phone: string,42 source: string43) {44 const { error } = await supabase.from('contacts').insert({45 owner_id: ownerId,46 name,47 company,48 email,49 phone,50 source,51 })52 if (error) throw new Error(error.message)53 revalidatePath('/contacts')54}5556export async function createDeal(57 contactId: string,58 pipelineId: string,59 title: string,60 value: number,61 stage: string,62 ownerId: string63) {64 const { error } = await supabase.from('deals').insert({65 contact_id: contactId,66 pipeline_id: pipelineId,67 title,68 value,69 stage,70 owner_id: ownerId,71 })72 if (error) throw new Error(error.message)73 revalidatePath('/deals')74}Customization ideas
Add email integration
Connect to Gmail or Outlook via API to log emails to deal activities automatically based on contact email address matching.
Add deal automation rules
Create automation rules that trigger when a deal enters a stage (e.g., send a proposal email when deal moves to Proposal stage) using Supabase triggers and Server Actions.
Add reporting dashboard
Build a reporting page with charts for deals won/lost over time, average deal cycle length, win rate by source, and top-performing sales reps.
Add team collaboration
Add org_id to all tables and update RLS policies to allow team members within the same organization to view and collaborate on deals and contacts.
Common pitfalls
Pitfall: Not using optimistic updates for drag-and-drop stage changes
How to avoid: Update the local state immediately on drop using React state, then fire the Server Action in the background. If the action fails, revert the state and show an error toast.
Pitfall: Querying Supabase without owner_id filtering
How to avoid: Always include .eq('owner_id', userId) in Supabase queries in addition to RLS policies. RLS is defense-in-depth, not a replacement for proper query scoping.
Pitfall: Storing pipeline stages as separate database rows instead of JSONB
How to avoid: Store stages as a JSONB array in the pipelines table. This makes retrieval a single query and reordering is a simple array manipulation.
Best practices
- Use @hello-pangea/dnd for Kanban drag-and-drop — it is the maintained fork of react-beautiful-dnd and works with React Server Components
- Store pipeline stages as JSONB for easy customization and ordering without complex table joins
- Use Server Components for contact lists and deal tables to keep database queries server-side
- Use RLS policies scoped by owner_id matching auth.uid() for multi-tenant data isolation
- Use Design Mode (Option+D) to adjust Kanban column widths, Card styling, and Badge colors without spending credits
- Log every customer interaction as an activity to build a complete relationship history
- Use revalidatePath after every mutation to keep the Kanban board and contact list current
- Implement global search with Supabase full-text search and shadcn/ui Command for a professional UX
AI prompts to try
Copy these prompts to build this project faster.
I'm building a pipeline CRM with Next.js App Router and Supabase. I need a Kanban deal board with drag-and-drop, contact management, activity logging, and revenue forecasting. Help me design the schema with JSONB pipeline stages and RLS policies scoped by owner_id.
Build a Kanban board component with @hello-pangea/dnd that renders pipeline stages as droppable columns and deals as draggable Cards. On drag end, optimistically update the local state to move the Card to the new column, then call a Server Action to update the deal's stage in Supabase. If the action fails, revert the Card to its original column and show a toast error.
Frequently asked questions
What is the best drag-and-drop library for a V0 Kanban board?
@hello-pangea/dnd is recommended — it is the actively maintained fork of react-beautiful-dnd with full React 18 support. V0 can install it via the project dependencies and generate the DragDropContext, Droppable, and Draggable components.
How do I handle multi-user access in the CRM?
Use Supabase RLS policies scoped by owner_id matching auth.uid(). Each user only sees their own contacts and deals. For team access, add an org_id column and update policies to allow team members within the same organization.
What V0 plan do I need for a CRM system?
V0 Premium is recommended because the CRM requires multiple complex pages (Kanban board, contacts, deal detail, forecast), drag-and-drop components, and several Server Actions.
Can I customize the pipeline stages?
Yes. Stages are stored as a JSONB array in the pipelines table. Add a settings page where users can add, remove, and reorder stages by modifying the JSONB array via a Server Action.
How do I deploy the CRM?
Click Share then Publish to Production in V0 for instant Vercel deployment. All data is stored in Supabase with RLS ensuring multi-user data isolation.
Can I import existing contacts from a spreadsheet?
Yes. Add a CSV import page similar to the budgeting tool pattern — upload a CSV, parse it with papaparse, map columns to contact fields, and bulk-insert into Supabase.
Can RapidDev help build a custom CRM?
Yes. RapidDev has built 600+ apps including CRMs with email integration, deal automation, multi-team pipelines, and advanced reporting. Book a free consultation to discuss your CRM requirements.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation