Build a full CRM system with Lovable and Supabase in 2–3 hours. You'll get a Kanban pipeline with drag-and-drop deal cards, a searchable contacts DataTable, real-time deal movements across team members, activity logging, and role-based team isolation — all without writing backend code manually.
What you're building
A CRM built in Lovable stores all your sales data in Supabase PostgreSQL with four core tables: contacts, deals, activities, and pipelines. Row-Level Security ensures every team member only sees records they own or that belong to their organization.
The pipeline view is a Kanban board where deal cards live in columns that map to your pipeline stages. When you drag a card from Proposal to Won, Lovable fires an optimistic update so the UI snaps instantly, then persists the change via a Supabase upsert. Supabase Realtime broadcasts the change to every other open session — no refresh needed.
The contacts view uses a shadcn/ui DataTable backed by TanStack Table v8. Clicking a row opens a Sheet panel on the right showing full contact details, linked deals, and a chronological activity feed. All heavy lifting is done by Supabase queries that Lovable generates from your prompts.
Final result
A team-ready CRM with Kanban pipeline, contacts directory, real-time collaboration, and full activity history — deployable from Lovable in one click.
Tech stack
Prerequisites
- Lovable Pro account (drag-and-drop Kanban needs enough credits for multi-step generation)
- Supabase project created at supabase.com (free tier works)
- Supabase URL and anon key ready to paste into Lovable Secrets
- Basic familiarity with Lovable's Cloud tab and Secrets panel
- Optional: a list of real pipeline stage names you want to use
Build steps
Scaffold the Supabase schema and connect to Lovable
Start by asking Lovable to generate the full database schema. It will create the SQL migration and wire up the Supabase client. After generation, paste your Supabase URL and anon key into Cloud tab → Secrets as VITE_SUPABASE_URL and VITE_SUPABASE_ANON_KEY.
1Create a CRM application with Supabase. Set up these tables:2- contacts: id, org_id, first_name, last_name, email, phone, company, owner_id, created_at3- pipelines: id, org_id, name, created_at4- deals: id, org_id, contact_id, pipeline_id, title, value, stage, owner_id, position, created_at, updated_at5- activities: id, org_id, contact_id, deal_id, type (call|email|note|meeting), body, created_by, created_at67Enable RLS on all tables. Add policies so users can only read/write rows where org_id matches their profile's org_id. Use Supabase Auth for authentication.Pro tip: Ask Lovable to generate a seed SQL snippet with 3 sample contacts and 5 sample deals so you can see the Kanban populated immediately without manual data entry.
Expected result: Lovable creates src/integrations/supabase/client.ts, generates TypeScript types for all four tables, and shows a basic scaffold in the preview.
Build the Kanban pipeline board with drag-and-drop
Prompt Lovable to render the deals table as a Kanban board. Each pipeline stage becomes a column. Deal cards show the contact name, deal value, and a Badge for the stage. Drag-and-drop uses optimistic UI updates before the Supabase upsert completes.
1Build a Kanban pipeline board component at src/components/pipeline/KanbanBoard.tsx.23Requirements:4- Fetch deals from Supabase grouped by stage5- Render columns for stages: Lead, Qualified, Proposal, Negotiation, Won, Lost6- Each card shows: contact name, deal title, formatted value (e.g. $12,500), owner avatar7- Use shadcn/ui Card for each deal card8- Use shadcn/ui Badge with color coding: Lead=gray, Qualified=blue, Proposal=yellow, Negotiation=orange, Won=green, Lost=red9- Implement drag-and-drop: when a card is dropped into a new column, optimistically update the UI immediately, then call supabase.from('deals').update({ stage: newStage }).eq('id', dealId)10- Show a loading skeleton while deals are fetching11- Add a + button at the top of each column to create a new deal in that stagePro tip: If Lovable struggles with the drag-and-drop in one prompt, break it into two: first build the static Kanban layout, then add drag-and-drop in a follow-up prompt.
Expected result: A fully interactive Kanban board appears in the preview. You can drag deal cards between columns and the stage updates persist in Supabase.
Add Supabase Realtime so deal movements sync live
Enable Realtime on the deals table in Supabase, then ask Lovable to subscribe to changes. When any team member moves a deal, every other open browser session updates automatically without a page refresh.
1Add Supabase Realtime to the KanbanBoard component. Subscribe to postgres_changes on the deals table. When an UPDATE event fires, update the local deals state to reflect the new stage and position. Clean up the subscription on component unmount. Show a subtle toast notification (use Sonner) when a deal is moved by another user, e.g. 'John moved Acme Corp deal to Won'.Expected result: Open the app in two browser tabs. Moving a deal in one tab causes the other tab to update within one second without refreshing.
Create the contacts DataTable with Sheet preview
Build the contacts view using shadcn/ui DataTable powered by TanStack Table v8. Clicking any row opens a Sheet panel on the right side showing full contact details, linked deals, and the activity timeline.
1import { useState } from 'react'2import { useQuery } from '@tanstack/react-query'3import { supabase } from '@/integrations/supabase/client'4import {5 Sheet,6 SheetContent,7 SheetHeader,8 SheetTitle,9} from '@/components/ui/sheet'10import { Badge } from '@/components/ui/badge'11import { Avatar, AvatarFallback } from '@/components/ui/avatar'12import { DataTable } from '@/components/ui/data-table'13import type { ColumnDef } from '@tanstack/react-table'1415type Contact = {16 id: string17 first_name: string18 last_name: string19 email: string20 company: string21 owner_id: string22}2324const columns: ColumnDef<Contact>[] = [25 {26 accessorFn: (row) => `${row.first_name} ${row.last_name}`,27 header: 'Name',28 cell: ({ getValue, row }) => (29 <div className="flex items-center gap-2">30 <Avatar className="h-7 w-7">31 <AvatarFallback>{row.original.first_name[0]}</AvatarFallback>32 </Avatar>33 <span>{getValue() as string}</span>34 </div>35 ),36 },37 { accessorKey: 'email', header: 'Email' },38 { accessorKey: 'company', header: 'Company' },39]4041export function ContactsTable() {42 const [selected, setSelected] = useState<Contact | null>(null)4344 const { data: contacts = [] } = useQuery({45 queryKey: ['contacts'],46 queryFn: async () => {47 const { data, error } = await supabase.from('contacts').select('*').order('created_at', { ascending: false })48 if (error) throw error49 return data as Contact[]50 },51 })5253 return (54 <>55 <DataTable columns={columns} data={contacts} onRowClick={setSelected} />56 <Sheet open={!!selected} onOpenChange={() => setSelected(null)}>57 <SheetContent className="w-[420px]">58 <SheetHeader>59 <SheetTitle>{selected?.first_name} {selected?.last_name}</SheetTitle>60 </SheetHeader>61 <div className="mt-4 space-y-2 text-sm">62 <p className="text-muted-foreground">{selected?.email}</p>63 <Badge variant="outline">{selected?.company}</Badge>64 </div>65 </SheetContent>66 </Sheet>67 </>68 )69}Pro tip: Ask Lovable to add a global search input above the table that filters contacts by name, email, or company in real time using TanStack Table's built-in filtering.
Expected result: The contacts page shows a paginated, searchable table. Clicking a row slides open the Sheet panel with contact details and linked deals.
Add the deal edit Dialog and activity log
Ask Lovable to build a Dialog that opens when you click a deal card. It should show all deal fields in an editable Form, and below the form display a chronological activity log with an input to add new activities.
1Build a DealDialog component at src/components/pipeline/DealDialog.tsx.23Requirements:4- Accept a deal prop and an onClose callback5- Show a shadcn/ui Dialog with two sections: Deal Info (form) and Activity Log6- Deal Info form fields: title (Input), value (Input, number), stage (Select with the 6 stages), owner (Select from team members), contact (Select from contacts)7- Use react-hook-form + zod for validation8- On submit, call supabase.from('deals').update(...).eq('id', deal.id)9- Activity Log section: show activities filtered by deal_id, ordered by created_at desc10- Each activity row shows: type Badge (call/email/note/meeting), body text, created_by name, relative time (e.g. '2 hours ago')11- Add a Textarea at the bottom to log a new activity, with a Submit button that inserts into the activities tableExpected result: Clicking any deal card opens the Dialog. You can edit fields, save changes, and add activity notes that appear immediately in the log.
Publish and configure RLS for team use
Before sharing with your team, verify RLS policies are active so each user only sees their organization's data. Then publish from the Publish icon in the top-right corner of Lovable.
1Review and harden the RLS policies for production. For each table (contacts, deals, activities, pipelines), ensure:21. Row-Level Security is enabled32. There is a SELECT policy: 'auth.uid() = owner_id OR org_id = (SELECT org_id FROM profiles WHERE id = auth.uid())'43. There is an INSERT policy that sets org_id automatically from the user's profile54. There is an UPDATE policy that matches the SELECT policy65. Add a profiles table if it doesn't exist: id (references auth.users), org_id, full_name, avatar_url78Show me the SQL for all policies.Pro tip: Test RLS by creating two users in different organizations in the Supabase Auth dashboard. Log in as each and confirm they cannot see each other's data.
Expected result: Supabase Table Editor shows the lock icon on all four tables. Running a query as a non-owner user returns zero rows.
Complete code
1import { Badge } from '@/components/ui/badge'2import { Card, CardContent } from '@/components/ui/card'3import { Avatar, AvatarFallback } from '@/components/ui/avatar'45type Deal = {6 id: string7 title: string8 value: number9 stage: string10 contact_name: string11 owner_initials: string12}1314const stageVariant: Record<string, 'default' | 'secondary' | 'outline' | 'destructive'> = {15 Lead: 'secondary',16 Qualified: 'default',17 Proposal: 'outline',18 Negotiation: 'outline',19 Won: 'default',20 Lost: 'destructive',21}2223interface DealCardProps {24 deal: Deal25 onClick: (deal: Deal) => void26 isDragging?: boolean27}2829export function DealCard({ deal, onClick, isDragging }: DealCardProps) {30 const formatted = new Intl.NumberFormat('en-US', {31 style: 'currency',32 currency: 'USD',33 maximumFractionDigits: 0,34 }).format(deal.value)3536 return (37 <Card38 className={`cursor-pointer transition-shadow hover:shadow-md ${39 isDragging ? 'opacity-50 rotate-1' : ''40 }`}41 onClick={() => onClick(deal)}42 >43 <CardContent className="p-3 space-y-2">44 <p className="text-sm font-medium leading-tight">{deal.title}</p>45 <p className="text-xs text-muted-foreground">{deal.contact_name}</p>46 <div className="flex items-center justify-between">47 <span className="text-sm font-semibold">{formatted}</span>48 <div className="flex items-center gap-1">49 <Badge variant={stageVariant[deal.stage] ?? 'secondary'} className="text-xs">50 {deal.stage}51 </Badge>52 <Avatar className="h-5 w-5">53 <AvatarFallback className="text-xs">{deal.owner_initials}</AvatarFallback>54 </Avatar>55 </div>56 </div>57 </CardContent>58 </Card>59 )60}Customization ideas
Email sequence automation
Add a Supabase Edge Function triggered when a deal moves to Qualified that sends a templated follow-up email via Resend. Store email templates in a sequences table and let users customize them per pipeline.
Revenue forecast chart
Add a dashboard page that aggregates deal values by expected close month and renders a bar chart with Recharts. Color code bars by stage (Proposal = yellow, Negotiation = orange, Won = green) so your team can spot forecast gaps at a glance.
Deal score from AI
Add an Edge Function that calls an LLM with the deal's activity log and returns a 0–100 score with a one-line reason. Display the score as a Progress bar on each Kanban card so reps know which deals need attention.
Custom pipeline stages per team
Let each organization configure its own stage names and colors. Store them in a pipeline_stages table linked to the pipelines table. The Kanban board reads stage names dynamically so no code changes are needed when stages change.
Mobile-optimized list view
Add a list view toggle (list/kanban) that switches to a stacked Card layout optimized for mobile. Sales reps in the field need to log activities from their phones without managing a full Kanban board.
Common pitfalls
Pitfall: Forgetting to enable RLS before sharing with the team
How to avoid: In the Supabase Table Editor, click the table name → RLS tab → Enable RLS. Then add policies. Ask Lovable to generate the policies for you by describing the access rules.
Pitfall: Placing the Supabase service role key in client-side code
How to avoid: Only the anon key goes in VITE_SUPABASE_ANON_KEY. The service role key should only appear in Edge Functions via Deno.env.get('SUPABASE_SERVICE_ROLE_KEY'), stored in Cloud tab → Secrets.
Pitfall: Running into AI looping when building the drag-and-drop Kanban
How to avoid: Switch to Plan Mode (no code changes) to let Lovable reason through the approach first. If still stuck after 3 attempts, duplicate the project and restart the Kanban step with a simpler prompt.
Pitfall: Not adding a Realtime cleanup function
How to avoid: Always return a cleanup function from the useEffect that calls channel.unsubscribe(). Ask Lovable explicitly to 'add cleanup on component unmount' in the Realtime prompt.
Pitfall: Skipping the position field on deals for Kanban ordering
How to avoid: Add a position integer column to the deals table. When a deal is dragged, update the positions of all affected cards in a single batch upsert.
Best practices
- Always enable RLS on every table before adding real data. Add policies that match your org_id structure so team isolation works from day one.
- Use optimistic UI updates for drag-and-drop so the board feels instant. Revert the optimistic update in the catch block if the Supabase call fails.
- Scope Realtime subscriptions per organization using a filter: .filter('org_id=eq.' + orgId) so clients only receive events relevant to them.
- Store sensitive API keys (like email provider keys) in Cloud tab → Secrets, not in VITE_ env vars. Secrets are encrypted and only accessible in Edge Functions.
- Break large Lovable prompts into steps: schema first, then layout, then interactivity. Each step builds on the previous verified output.
- Add a soft-delete pattern (deleted_at timestamp) instead of hard deletes on contacts and deals. This makes accidental deletion recoverable.
- Use TanStack Query's staleTime to avoid refetching the contacts list on every mount. Set staleTime: 60_000 for list views and rely on Realtime for live updates.
AI prompts to try
Copy these prompts to build this project faster.
I'm building a CRM in Lovable (React + Supabase). I have tables: contacts, deals, activities, pipelines. Help me design the RLS policies for team-based isolation where users belong to an organization (org_id on their profile). Each user should read/write only their org's rows. Give me the SQL for SELECT, INSERT, UPDATE, and DELETE policies for each table.
Add a global activity feed page at /activities that shows all recent activities across all contacts and deals for the current org. Use a shadcn/ui ScrollArea with infinite scroll. Each item shows: activity type Badge, contact name (clickable, opens Sheet), deal title if linked, body text, and relative timestamp. Fetch from Supabase with a join on contacts and deals. Subscribe to Realtime so new activities appear at the top without refresh.
In Supabase, write a PostgreSQL function called get_pipeline_summary(p_pipeline_id uuid) that returns a JSON object with: total_deals count, total_value sum, value_by_stage as an object keyed by stage name, and deals_won_this_month count. This function should respect RLS by running with SECURITY INVOKER so it uses the calling user's permissions.
Frequently asked questions
Can I import existing contacts from a CSV into my Lovable CRM?
Yes. Ask Lovable to add a CSV import feature — it will generate a file input, a Papa Parse integration to read the CSV, a preview table using shadcn/ui DataTable, and a batch insert into your contacts table in Supabase. Lovable handles the column mapping UI so users can match their CSV headers to your schema.
How do I prevent team members from seeing each other's deals?
Enable RLS on your deals table and create a SELECT policy that checks org_id against the user's profile. For individual ownership, add a secondary condition: owner_id = auth.uid() OR org_id = (SELECT org_id FROM profiles WHERE id = auth.uid()). Ask Lovable to generate these policies and it will write the SQL and apply the migration.
Will Supabase Realtime work after I deploy the app?
Yes, Supabase Realtime works on all Supabase plans including free tier. The WebSocket connection is established from the user's browser directly to Supabase, so it works regardless of where your Lovable app is deployed. Just make sure the table's Realtime option is toggled on in the Supabase dashboard under Database → Replication.
Can I add email notifications when a deal stage changes?
Yes. Create a Supabase Edge Function that receives a webhook from a Supabase Database Webhook (triggered on deal UPDATE). The function checks if the stage column changed, then sends an email via Resend or SendGrid using the deal owner's email from the profiles table. Store your email provider API key in Cloud tab → Secrets.
What happens if I accidentally delete a contact?
By default, a delete is permanent. To make it recoverable, ask Lovable to add a deleted_at timestamptz column to your contacts table and change DELETE buttons to set deleted_at = now() instead. Update your RLS SELECT policy to add AND deleted_at IS NULL so soft-deleted rows are invisible to the app but remain in the database.
Can Lovable handle a CRM with thousands of contacts?
Yes, as long as you use pagination or virtual scrolling. Ask Lovable to implement server-side pagination in the contacts DataTable: fetch only 50 rows per page using Supabase's .range(from, to) and pass total count via select('*', { count: 'exact' }). TanStack Table supports server-side pagination natively.
Is there a team or agency that can help set up a custom CRM in Lovable?
RapidDev specializes in building production-grade Lovable apps including CRMs with custom workflows, third-party integrations, and data migrations. Reach out if you need a bespoke setup that goes beyond what you can prompt yourself.
Can I connect my CRM to an email provider like Gmail or Outlook?
You can connect via OAuth using a Supabase Edge Function as the proxy. The function handles the OAuth token exchange and stores refresh tokens encrypted in a user_integrations table. From there, the function can fetch emails using the Gmail or Microsoft Graph API and insert them as activities linked to matching contacts by email address.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation