Skip to main content
RapidDev - Software Development Agency

How to Build a Project Management Tool with Lovable

Build a full project management tool in Lovable with Kanban and list views by setting up Supabase tables for projects, boards, columns, and tasks, then using drag-and-drop with optimistic updates for instant responsiveness. You get real-time collaboration, task detail dialogs, and a clean shadcn/ui interface — no backend coding required.

What you'll build

  • Kanban board with drag-and-drop cards organized into columns
  • List view with sortable DataTable for all tasks across a project
  • Task detail Dialog with description, assignee, due date, priority, and labels
  • Real-time updates so team members see moves and edits instantly
  • Project member management with role-based access
  • Board management — create, rename, and reorder columns
  • Project overview page with task count stats per column
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 full project management tool in Lovable with Kanban and list views by setting up Supabase tables for projects, boards, columns, and tasks, then using drag-and-drop with optimistic updates for instant responsiveness. You get real-time collaboration, task detail dialogs, and a clean shadcn/ui interface — no backend coding required.

What you're building

A project management tool gives teams a shared space to organize work visually. The Kanban view — columns with draggable task cards — is the core interaction: you can see the full workflow at a glance and move tasks through stages by dragging. The list view gives a more structured, sortable table format for when you need to filter by assignee, due date, or priority.

The hardest technical challenge in this build is optimistic drag-and-drop: when a user drags a card, the UI should respond instantly even before the Supabase update confirms. If the update fails, the card should snap back. Lovable handles this pattern well when prompted correctly — the key is updating local state first, then persisting to the database.

Real-time collaboration is powered by Supabase Realtime. When one team member moves a task, all other members with the board open see the change within a second. This is implemented as a Postgres changes subscription that triggers a state update.

Final result

A fully collaborative project management tool with Kanban and list views, real-time updates, and task management features comparable to Trello.

Tech stack

LovableFrontend builder and code generation
Supabase PostgreSQLDatabase for projects, boards, columns, tasks, members
Supabase AuthUser authentication
Supabase RealtimeLive task and board updates across team members
shadcn/uiCard, Dialog, DataTable, Tabs, Badge, Select components
React Hook Form + ZodTask creation and edit form validation

Prerequisites

  • A Lovable account (Pro plan for multiple Supabase tables and Realtime)
  • A Supabase project with URL and anon key available
  • A Lovable project created and connected to your Supabase project via Cloud tab
  • Basic familiarity with how Supabase tables and rows work
  • Optional: A list of your project stages (e.g., Todo, In Progress, Review, Done) ready to use as default columns

Build steps

1

Create the database schema for projects, boards, and tasks

Set up five tables: projects, boards, columns, tasks, and project_members. The position column on both columns and tasks is a float (not integer) so you can insert between existing items without renumbering everything — this is the standard trick for drag-and-drop ordering.

supabase-schema.sql
1-- Run in Supabase SQL Editor
2
3CREATE TYPE task_priority AS ENUM ('low', 'medium', 'high', 'urgent');
4
5CREATE TABLE projects (
6 id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
7 name TEXT NOT NULL,
8 description TEXT,
9 owner_id UUID REFERENCES auth.users NOT NULL,
10 created_at TIMESTAMPTZ DEFAULT now()
11);
12
13CREATE TABLE project_members (
14 project_id UUID REFERENCES projects(id) ON DELETE CASCADE NOT NULL,
15 user_id UUID REFERENCES auth.users NOT NULL,
16 role TEXT NOT NULL DEFAULT 'member' CHECK (role IN ('owner', 'admin', 'member', 'viewer')),
17 joined_at TIMESTAMPTZ DEFAULT now(),
18 PRIMARY KEY (project_id, user_id)
19);
20
21CREATE TABLE boards (
22 id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
23 project_id UUID REFERENCES projects(id) ON DELETE CASCADE NOT NULL,
24 name TEXT NOT NULL DEFAULT 'Main Board',
25 created_at TIMESTAMPTZ DEFAULT now()
26);
27
28CREATE TABLE columns (
29 id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
30 board_id UUID REFERENCES boards(id) ON DELETE CASCADE NOT NULL,
31 name TEXT NOT NULL,
32 position FLOAT NOT NULL DEFAULT 0,
33 created_at TIMESTAMPTZ DEFAULT now()
34);
35
36CREATE TABLE tasks (
37 id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
38 column_id UUID REFERENCES columns(id) ON DELETE CASCADE NOT NULL,
39 project_id UUID REFERENCES projects(id) NOT NULL,
40 title TEXT NOT NULL,
41 description TEXT,
42 assignee_id UUID REFERENCES auth.users,
43 priority task_priority DEFAULT 'medium',
44 due_date DATE,
45 labels TEXT[] DEFAULT '{}',
46 position FLOAT NOT NULL DEFAULT 0,
47 created_by UUID REFERENCES auth.users NOT NULL,
48 created_at TIMESTAMPTZ DEFAULT now(),
49 updated_at TIMESTAMPTZ DEFAULT now()
50);
51
52-- Enable RLS
53ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
54ALTER TABLE project_members ENABLE ROW LEVEL SECURITY;
55ALTER TABLE boards ENABLE ROW LEVEL SECURITY;
56ALTER TABLE columns ENABLE ROW LEVEL SECURITY;
57ALTER TABLE tasks ENABLE ROW LEVEL SECURITY;
58
59-- Projects: members can see projects they belong to
60CREATE POLICY "Members see projects" ON projects FOR SELECT USING (
61 owner_id = auth.uid() OR
62 EXISTS (SELECT 1 FROM project_members WHERE project_id = id AND user_id = auth.uid())
63);
64CREATE POLICY "Owners create projects" ON projects FOR INSERT WITH CHECK (owner_id = auth.uid());
65CREATE POLICY "Owners update projects" ON projects FOR UPDATE USING (owner_id = auth.uid());
66
67-- Tasks: project members see all tasks in their projects
68CREATE POLICY "Members see project tasks" ON tasks FOR SELECT USING (
69 EXISTS (SELECT 1 FROM project_members WHERE project_id = tasks.project_id AND user_id = auth.uid())
70 OR EXISTS (SELECT 1 FROM projects WHERE id = tasks.project_id AND owner_id = auth.uid())
71);
72CREATE POLICY "Members create tasks" ON tasks FOR INSERT WITH CHECK (
73 EXISTS (SELECT 1 FROM project_members WHERE project_id = tasks.project_id AND user_id = auth.uid())
74 OR EXISTS (SELECT 1 FROM projects WHERE id = tasks.project_id AND owner_id = auth.uid())
75);
76CREATE POLICY "Members update tasks" ON tasks FOR UPDATE USING (
77 EXISTS (SELECT 1 FROM project_members WHERE project_id = tasks.project_id AND user_id = auth.uid())
78 OR EXISTS (SELECT 1 FROM projects WHERE id = tasks.project_id AND owner_id = auth.uid())
79);
80
81-- Boards and columns: same project membership rule
82CREATE POLICY "Members see boards" ON boards FOR SELECT USING (
83 EXISTS (SELECT 1 FROM project_members WHERE project_id = boards.project_id AND user_id = auth.uid())
84 OR EXISTS (SELECT 1 FROM projects WHERE id = boards.project_id AND owner_id = auth.uid())
85);
86CREATE POLICY "Members see columns" ON columns FOR SELECT USING (
87 EXISTS (SELECT 1 FROM boards b JOIN project_members pm ON b.project_id = pm.project_id
88 WHERE b.id = columns.board_id AND pm.user_id = auth.uid())
89);
90CREATE POLICY "Members manage columns" ON columns FOR ALL USING (
91 EXISTS (SELECT 1 FROM boards b JOIN project_members pm ON b.project_id = pm.project_id
92 WHERE b.id = columns.board_id AND pm.user_id = auth.uid())
93);

Pro tip: The float position trick works like this: if you have items at positions 1.0, 2.0, 3.0 and want to insert between 1 and 2, set position to 1.5. If you need to insert between 1.5 and 2.0, use 1.75. This means you almost never need to renumber everything — only if positions get too close (difference < 0.001).

Expected result: Five tables appear in Supabase Table Editor, all with the RLS lock icon. SQL Editor shows no errors.

2

Build the Kanban board with drag-and-drop task cards

The Kanban view is the main interaction. Ask Lovable to create a horizontal scrollable board with column headers, task cards using shadcn Card components, and drag-and-drop using the HTML5 drag API. Each card shows title, priority badge, assignee avatar, and due date.

prompt.txt
1Build a Kanban board view for the project management tool:
2
31. Board layout: horizontal scrollable container, each column is 280px wide with a fixed header
42. Column header: column name + task count badge + "Add task" button (+ icon)
53. Task cards (shadcn Card, draggable):
6 - Title (bold, max 2 lines with overflow-hidden)
7 - Priority badge (low=gray, medium=blue, high=orange, urgent=red)
8 - Due date (red text if overdue)
9 - Assignee avatar (shadcn Avatar, initials fallback)
10 - Drag handle icon on the left
11
124. Drag-and-drop behavior:
13 - Use HTML5 draggable attribute (draggable={true})
14 - onDragStart: store task ID and source column ID in state
15 - onDragOver on column: preventDefault() to allow drop, highlight column with bg-blue-50
16 - onDrop: calculate new position (between previous and next task positions), update local state immediately (optimistic), then call Supabase update in background
17 - If Supabase update fails, revert local state and show error toast
18
195. Quick add task: clicking "Add task" shows an inline input at the bottom of the column. Press Enter or click the checkmark to create. Position = last task position + 1.
20
21Fetch columns ordered by position, tasks ordered by position within each column from Supabase.
22Group tasks by column_id in a Map for rendering.

Pro tip: When Lovable implements the drag-and-drop, ask it to debounce the Supabase update by 500ms. This prevents a flood of API calls when a user drags quickly across multiple columns before settling.

Expected result: The Kanban board renders all columns and task cards. Dragging a card to a new column moves it visually instantly, then saves to Supabase. Refreshing the page confirms the card is in the new column.

3

Add the list view with sortable DataTable

The list view shows all tasks in a project as a sortable DataTable. Users can toggle between Kanban and list views using a tab selector or toggle buttons. The list view is more useful for filtering by assignee or due date.

prompt.txt
1Add a list view to the project page that can be toggled alongside the Kanban view:
2
31. View toggle: two icon buttons in the top-right of the project page (grid icon = Kanban, list icon = List). Save preference to localStorage.
4
52. List view: shadcn DataTable with columns:
6 - Title (clickable, opens task detail Dialog)
7 - Column/Stage (the column this task is in)
8 - Priority (Badge with color)
9 - Assignee (Avatar + name)
10 - Due Date (formatted, red if overdue)
11 - Labels (small chips)
12 - Actions (Edit, Move to column dropdown, Delete)
13
143. Toolbar above the table:
15 - Filter by assignee (Select with project members)
16 - Filter by priority (Select)
17 - Filter by column/stage (Select)
18 - Search by task title (Input)
19 - "New Task" button opening a Dialog
20
214. Sorting: click column headers to sort ascending/descending
22
23Fetch all tasks for the current project joined with columns for stage names and auth users for assignee names. Apply filters in JavaScript after fetch.

Expected result: Toggling to list view shows all tasks in a clean DataTable. Clicking a column header sorts the tasks. The search input filters rows in real time. Clicking a task title opens the detail dialog.

4

Build the task detail Dialog with full editing

Clicking any task card or row opens a Dialog with the full task details. The dialog is the primary editing surface: update title, description, assignee, priority, due date, and labels. All changes auto-save with a 1-second debounce to Supabase.

prompt.txt
1Create a full task detail Dialog component that opens when clicking any task:
2
31. Dialog size: max-w-2xl, full height on mobile
42. Left column (2/3 width):
5 - Title: contentEditable h1 with inline editing (no separate edit button)
6 - Description: shadcn Textarea with markdown hint, auto-resize
7 - Labels: input to type new labels + X buttons to remove existing
8 - Activity log: list of recent changes (placeholder text for now)
9
103. Right column (1/3 width) - metadata sidebar:
11 - Assignee: shadcn Select showing project members with avatars
12 - Priority: shadcn Select (low/medium/high/urgent with colored dots)
13 - Due date: shadcn Calendar via Popover
14 - Stage/Column: shadcn Select showing board columns
15 - Created by and created date (read-only)
16
174. Auto-save: use useEffect with a 1-second debounce on any field change. Show a small "Saving..." / "Saved" indicator in the dialog footer.
18
195. Delete button: bottom-left, with a confirmation AlertDialog before deleting
20
21All changes update the local task state immediately (optimistic) and then call Supabase update. Moving to a different column updates column_id and sets position to last + 1 in the target column.

Pro tip: Ask Lovable to use a useRef for the debounce timer so the auto-save doesn't re-create the timeout on every render. The pattern is: clearTimeout(timerRef.current); timerRef.current = setTimeout(() => saveToSupabase(data), 1000);

Expected result: Clicking a task opens the dialog pre-filled with all data. Editing the title and waiting 1 second shows 'Saved'. Closing and reopening the dialog (or the list view) shows the updated title.

5

Enable real-time collaboration with Supabase Realtime

Subscribe to Postgres changes on the tasks and columns tables so all team members see updates instantly. Show presence indicators so users can see who else is viewing the board.

prompt.txt
1Add real-time updates to the Kanban board using Supabase Realtime:
2
31. Subscribe to postgres_changes on the 'tasks' table filtered by project_id:
4 - INSERT event: add the new task to the correct column in local state
5 - UPDATE event: update the task in local state (handle both content changes and column moves)
6 - DELETE event: remove the task from local state
7
82. Subscribe to postgres_changes on the 'columns' table:
9 - INSERT/UPDATE/DELETE: refetch all columns
10
113. Show a subtle "Live" indicator badge in the board header (green dot + "Live") when subscribed
12
134. Presence: use Supabase channel().track() to broadcast current user's name and avatar when they open the board. Show small avatar stack in the board header for all users currently viewing (max 5 shown, +N for the rest).
14
15Create the subscription in a useEffect that returns a cleanup function calling channel.unsubscribe().
16Use a single channel for the board: supabase.channel('board-' + boardId)
17Handle all event types on the same channel.
18
19IMPORTANT: Skip applying realtime updates to tasks that the current user just modified (check by user_id) to avoid double-applying optimistic updates.

Expected result: Opening the board in two browser tabs, moving a card in one tab causes it to move in the other tab within 1-2 seconds. Both tabs show the other user's avatar in the presence stack.

Complete code

src/components/board/TaskCard.tsx
1import { Card, CardContent } from '@/components/ui/card';
2import { Badge } from '@/components/ui/badge';
3import { Avatar, AvatarFallback } from '@/components/ui/avatar';
4import { GripVertical, CalendarDays } from 'lucide-react';
5import { format, isPast } from 'date-fns';
6
7type Priority = 'low' | 'medium' | 'high' | 'urgent';
8
9const PRIORITY_STYLES: Record<Priority, string> = {
10 low: 'bg-gray-100 text-gray-600',
11 medium: 'bg-blue-100 text-blue-700',
12 high: 'bg-orange-100 text-orange-700',
13 urgent: 'bg-red-100 text-red-700',
14};
15
16interface Task {
17 id: string;
18 title: string;
19 priority: Priority;
20 due_date?: string | null;
21 assignee_name?: string | null;
22 labels: string[];
23}
24
25interface TaskCardProps {
26 task: Task;
27 onClick: (taskId: string) => void;
28 onDragStart: (e: React.DragEvent, taskId: string, columnId: string) => void;
29 columnId: string;
30}
31
32export function TaskCard({ task, onClick, onDragStart, columnId }: TaskCardProps) {
33 const isOverdue = task.due_date ? isPast(new Date(task.due_date)) : false;
34
35 return (
36 <Card
37 className="cursor-pointer hover:shadow-md transition-shadow mb-2 group"
38 draggable
39 onDragStart={(e) => onDragStart(e, task.id, columnId)}
40 onClick={() => onClick(task.id)}
41 >
42 <CardContent className="p-3">
43 <div className="flex items-start gap-2">
44 <GripVertical className="h-4 w-4 text-gray-300 mt-0.5 opacity-0 group-hover:opacity-100 shrink-0" />
45 <div className="flex-1 min-w-0">
46 <p className="text-sm font-medium line-clamp-2 text-gray-900">{task.title}</p>
47 {task.labels.length > 0 && (
48 <div className="flex flex-wrap gap-1 mt-1.5">
49 {task.labels.slice(0, 3).map((label) => (
50 <span key={label} className="text-xs bg-gray-100 text-gray-600 px-1.5 py-0.5 rounded">
51 {label}
52 </span>
53 ))}
54 </div>
55 )}
56 <div className="flex items-center justify-between mt-2">
57 <Badge className={`text-xs ${PRIORITY_STYLES[task.priority]}`} variant="secondary">
58 {task.priority}
59 </Badge>
60 <div className="flex items-center gap-1.5">
61 {task.due_date && (
62 <span className={`flex items-center gap-0.5 text-xs ${isOverdue ? 'text-red-500' : 'text-gray-400'}`}>
63 <CalendarDays className="h-3 w-3" />
64 {format(new Date(task.due_date), 'MMM d')}
65 </span>
66 )}
67 {task.assignee_name && (
68 <Avatar className="h-5 w-5">
69 <AvatarFallback className="text-xs">
70 {task.assignee_name.split(' ').map((n) => n[0]).join('').slice(0, 2)}
71 </AvatarFallback>
72 </Avatar>
73 )}
74 </div>
75 </div>
76 </div>
77 </div>
78 </CardContent>
79 </Card>
80 );
81}

Customization ideas

Add time tracking to tasks

Add a task_time_logs table with start_time, end_time, and task_id. Add a timer button to the task detail Dialog that inserts a log on start and updates end_time on stop. Show total tracked time in the task card.

Build a sprint planning view

Add a sprints table and a sprint view that groups tasks into two-week sprints. Add velocity charts using Recharts showing completed vs planned tasks per sprint.

Add task dependencies

Create a task_dependencies table linking blocking and blocked tasks. Show a warning badge on tasks that are blocked and prevent them from being moved to Done while blockers are open.

Integrate Slack notifications

Create an Edge Function that sends a Slack webhook message when a task is assigned to someone or moved to a specific column (e.g., Done). Store the Slack webhook URL in Secrets.

Add a project timeline view

Build a Gantt-style timeline view using SVG or a charting library that shows tasks as horizontal bars between their start and due dates. Let users drag bar edges to adjust dates.

Enable file attachments on tasks

Add a task_attachments table and Supabase Storage bucket. Allow users to drag files into the task detail Dialog and attach them. Show a paperclip icon with attachment count on task cards.

Common pitfalls

Pitfall: Using integer positions for task ordering

How to avoid: Use float (FLOAT8) for position columns. Insert between two items by averaging their positions: new_position = (prev_position + next_position) / 2.

Pitfall: Applying real-time updates to tasks the current user just modified

How to avoid: Track recently-modified task IDs in a Set with a 2-second TTL. In the Realtime handler, skip updates for tasks in this set.

Pitfall: Forgetting to add the project to project_members after creating it

How to avoid: After inserting a project, also insert a row in project_members with the owner's user_id and role='owner'. Use a Supabase database trigger or do both inserts in the application code.

Pitfall: Fetching tasks for all projects on the board page

How to avoid: Always add .eq('project_id', currentProjectId) to all task queries. Use the board ID in Realtime subscriptions to filter events.

Best practices

  • Use float positions for task and column ordering to avoid full renumbering on every drag operation
  • Implement optimistic UI updates for drag-and-drop: update local state first, persist to Supabase second, revert on failure
  • Debounce auto-save in the task detail Dialog by 1 second to avoid a Supabase write on every keystroke
  • Add a database trigger to update tasks.updated_at automatically so activity logs always have accurate timestamps
  • Subscribe to Supabase Realtime channels scoped to the current board ID to avoid receiving irrelevant events
  • Cache project members in component state after initial fetch — they rarely change and don't need real-time updates
  • Limit label display to 3 on task cards with a +N overflow indicator to keep cards compact
  • Test drag-and-drop on touch devices — the HTML5 drag API doesn't work on mobile, so for mobile support ask Lovable to use pointer events instead

AI prompts to try

Copy these prompts to build this project faster.

ChatGPT Prompt

I'm building a Kanban board with drag-and-drop in React. Tasks have a 'position' float column. When a user drops a task between two other tasks, I need to calculate the new position value. Write a TypeScript function that takes the tasks array in a column, the dragged task ID, and the drop index, then returns the correct new position float value that places the task between the correct neighbors. Handle edge cases: dropping at the start, end, and between items.

Lovable Prompt

Refactor the Kanban board drag-and-drop to use the Pointer Events API instead of the HTML5 Drag and Drop API, so it works on both desktop and mobile touch screens. Maintain the same optimistic update behavior and Supabase persistence logic.

Build Prompt

In Lovable, add Supabase Realtime presence tracking to the Kanban board. When a user opens the board, broadcast their display name and avatar URL using channel.track(). Show a presence indicator in the board header with avatars of all users currently viewing this board. When a user closes the board or navigates away, they should automatically be removed from the presence list.

Frequently asked questions

Why does drag-and-drop not work on mobile devices?

The HTML5 Drag and Drop API is not supported on iOS and Android touch screens. For mobile support, ask Lovable to implement the same behavior using Pointer Events (onPointerDown, onPointerMove, onPointerUp) which work on all devices. This requires calculating the drop target manually based on element position, but Lovable can handle this logic.

How do I prevent real-time updates from conflicting with my own drag?

Keep a Set of task IDs that the current user recently modified, and in the Realtime event handler, skip applying updates to those task IDs for 2-3 seconds. This prevents the Realtime event from reverting your optimistic update before Supabase confirms it.

Can I have multiple boards in one project?

Yes — the schema supports this. Each project can have multiple boards via the boards table. Add a boards list in the project sidebar and let users switch between them. Each board has its own set of columns.

How do I deploy this to my own domain?

Click the Publish icon in the top-right of Lovable, then in the publish settings connect your custom domain. Lovable Pro plans support custom domains. Your Supabase tables, RLS policies, and Realtime subscriptions all continue working after publishing.

How many tasks can the board handle before it gets slow?

For performance, fetch only the first 100 tasks per column using .limit(100).order('position'). Add a 'Load more' button for columns with many tasks. For projects with thousands of tasks, consider virtual scrolling — ask Lovable to use a virtual list library for the Kanban columns.

How does the project membership system work?

Project owners can invite users by inserting a row in project_members with the user's ID. The invited user must already have a Supabase Auth account. For invitation flows, create an invitations table with email + project_id and send invite emails via Edge Function — ask Lovable to build this as an extension.

Can RapidDev help add more complex features like sprint planning or Gantt charts?

Yes — these are common extensions to Lovable-built project management tools. RapidDev can help design the sprint table schema and build the Gantt visualization on top of your existing Supabase setup.

What happens if two users drag the same task at the same time?

The last write wins at the database level. Supabase will accept both updates and the task will end up at whichever position was written last. To handle this gracefully, subscribe to Realtime updates and show a brief 'syncing' indicator when a position conflict is detected (the task's position in state differs from what Realtime sends).

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.