Build an HR management system with V0 using Next.js, Supabase for employee records, and PostgreSQL RPC for atomic leave approval. You'll create an employee directory, leave request workflow, onboarding checklists, and department analytics — all in about 2-4 hours without touching a terminal.
What you're building
HR management covers the employee lifecycle — from hiring and onboarding through daily leave requests and time tracking to department analytics. A centralized system replaces scattered spreadsheets with role-based access and automated workflows.
V0 generates the multi-page architecture quickly: employee directory, profile pages, leave approval queues, and analytics dashboards. Supabase handles employee records, leave balances, document storage, and RLS-based access control.
The architecture uses Next.js App Router with Server Components for data-heavy pages, a client component for the multi-step onboarding form, Server Actions for CRUD operations, a PostgreSQL RPC function for atomic leave approval that prevents negative balances, and Recharts for headcount and leave charts.
Final result
An HR management system with employee directory, leave request workflow with balance enforcement, onboarding checklists, document management, and department analytics.
Tech stack
Prerequisites
- A V0 account (Premium recommended for the multi-page architecture)
- A Supabase project with Storage enabled for employee documents
- Basic understanding of HR workflows (employees, departments, leave requests)
- Familiarity with role-based access (HR admin, manager, employee)
Build steps
Set up the project and HR database schema
Open V0 and create a new project. Connect Supabase via the Connect panel. Create the schema for departments, employees, leave requests, leave balances, onboarding tasks, time entries, and documents.
1// Paste this prompt into V0's AI chat:2// Build an HR management system. Create a Supabase schema with:3// 1. departments: id (uuid PK), name (text), head_id (uuid FK to employees nullable), parent_id (uuid FK to departments nullable), created_at (timestamptz)4// 2. employees: id (uuid PK), user_id (uuid FK to auth.users), department_id (uuid FK to departments), first_name (text), last_name (text), email (text), phone (text), job_title (text), employment_type (text check in 'full_time','part_time','contract'), start_date (date), end_date (date nullable), salary_cents (bigint), manager_id (uuid FK to employees nullable), status (text default 'active' check in 'active','on_leave','terminated','onboarding'), avatar_url (text), created_at (timestamptz)5// 3. leave_requests: id (uuid PK), employee_id (uuid FK to employees), type (text check in 'vacation','sick','personal','parental','unpaid'), start_date (date), end_date (date), days (decimal), reason (text), status (text default 'pending' check in 'pending','approved','rejected'), approved_by (uuid FK to employees nullable), created_at (timestamptz)6// 4. leave_balances: id (uuid PK), employee_id (uuid FK to employees), type (text), year (int), total_days (decimal), used_days (decimal default 0), unique(employee_id, type, year)7// 5. onboarding_tasks: id (uuid PK), employee_id (uuid FK to employees), title (text), description (text), assigned_to (uuid FK to employees), due_date (date), completed (boolean default false), position (int)8// 6. time_entries: id (uuid PK), employee_id (uuid FK to employees), date (date), hours (decimal), project (text), notes (text), created_at (timestamptz)9// 7. documents: id (uuid PK), employee_id (uuid FK to employees), title (text), file_url (text), type (text check in 'contract','id','certificate','other'), uploaded_at (timestamptz)10// Add RLS: employees see their own data, managers see their reports, HR admins see all.Pro tip: Connect to GitHub via the Git panel early — this system has many routes, so tracking changes with automatic branching helps manage the complex architecture.
Expected result: Database schema created with 7 tables, RLS policies for role-based access, and relationships between departments, employees, and their records.
Build the HR dashboard and employee directory
Create the main dashboard showing HR KPIs and the employee directory with search and filtering.
1// Paste this prompt into V0's AI chat:2// Build an HR dashboard at app/page.tsx.3// Requirements:4// - Stats Cards row: total employees, open leave requests count, employees onboarding, departments count5// - Recharts BarChart showing headcount by department (wrap in 'use client' component)6// - PieChart showing leave type distribution for the current month7// - Recent activity feed: latest leave requests, new hires, completed onboarding tasks8// - Navigation to /employees, /leave, /reports9//10// Build employee directory at app/employees/page.tsx:11// - Table with columns: Avatar + name, department, job title, status Badge, start date, actions12// - Column sorting on name and start_date13// - Search Input filtering by name or email14// - Select filter for department15// - Badge colors: active=green, on_leave=yellow, terminated=red, onboarding=blue16// - Click row to navigate to /employees/[id]17// - "Add Employee" Button linking to /employees/newExpected result: The dashboard shows headcount charts, leave distribution, and recent activity. The directory lists all employees with search, department filter, and status badges.
Build the employee profile with tabs
Create the employee detail page with tabbed sections for personal info, leave history, time tracking, documents, and onboarding tasks.
1// Paste this prompt into V0's AI chat:2// Build employee profile at app/employees/[id]/page.tsx.3// Requirements:4// - Header: Avatar, full name, job title, department Badge, status Badge5// - Tabs component with sections:6// 1. Info tab: Card with personal details (email, phone, employment type, start date, salary), edit Button for HR admins7// 2. Leave tab: Table of leave requests (type, dates, days, status Badge), "Request Leave" Button opening Dialog with DatePickerWithRange, Select for leave type, Textarea for reason8// 3. Time tab: Table of time entries this month, total hours Card, "Log Time" Button9// 4. Documents tab: list of uploaded documents with download links, "Upload" Button using file Input that uploads to Supabase Storage bucket 'employee-documents'10// 5. Onboarding tab (visible only for status='onboarding'): Checkbox list of onboarding tasks with assigned person and due date11// - Server Actions for leave request submission, time entry logging, document upload, onboarding task completion12// - Zod validation on all form inputsExpected result: Employee profile shows all HR data in organized tabs with forms for leave requests, time logging, document uploads, and onboarding task tracking.
Create the atomic leave approval system
Build the leave management page and the PostgreSQL RPC function that approves leave requests while atomically checking and deducting leave balances.
1// Paste this prompt into V0's AI chat:2// Build leave management at app/leave/page.tsx and create the approval RPC.3// Requirements:4// - Pending leave requests queue: Table showing employee name, leave type, dates, days requested, reason5// - Approve/Reject buttons per row6// - Create a PostgreSQL function approve_leave(request_id uuid, approver_id uuid):7// BEGIN;8// SELECT days, type, employee_id FROM leave_requests WHERE id = request_id AND status = 'pending' FOR UPDATE;9// SELECT total_days - used_days AS remaining FROM leave_balances WHERE employee_id = emp AND type = leave_type AND year = current_year FOR UPDATE;10// IF remaining < requested_days THEN RAISE EXCEPTION 'Insufficient leave balance';11// UPDATE leave_balances SET used_days = used_days + requested_days;12// UPDATE leave_requests SET status = 'approved', approved_by = approver_id;13// COMMIT;14// - API route at app/api/leave/approve/route.ts that calls this RPC15// - Calendar overlay showing approved leave per team (visible to managers)16// - Badge for leave status: pending=yellow, approved=green, rejected=red17// - AlertDialog for rejection requiring a reasonPro tip: The PostgreSQL RPC function uses FOR UPDATE row locks to prevent race conditions — even if two managers approve simultaneously, leave balances stay accurate.
Expected result: Managers can approve or reject leave requests from a queue. The RPC function atomically validates balances and prevents over-approval.
Build multi-step onboarding and reports
Create the new employee onboarding form and the HR reports page with headcount trends and leave analytics.
1// Paste this prompt into V0's AI chat:2// Build two pages:3// 1. app/employees/new/page.tsx — multi-step onboarding form ('use client'):4// - Step 1: Personal info (first_name, last_name, email, phone, avatar upload)5// - Step 2: Employment details (department Select, job_title, employment_type Select, start_date, salary)6// - Step 3: Manager assignment (Select from existing employees in the chosen department)7// - Step 4: Onboarding tasks (pre-filled checklist with common tasks, ability to add custom tasks)8// - Progress indicator showing current step9// - Server Action to insert employee + leave_balances (auto-create vacation/sick/personal with defaults) + onboarding_tasks in one transaction10//11// 2. app/reports/page.tsx — HR analytics:12// - Recharts LineChart: headcount trend over 12 months13// - BarChart: leave utilization by department14// - PieChart: employment type distribution15// - Cards: average tenure, turnover rate this year, departments with highest leave usage16// - Select for time period filtering17// - Wrap all charts in 'use client' componentsExpected result: HR admins can onboard new employees through a guided multi-step form. The reports page shows headcount trends, leave utilization, and workforce analytics.
Complete code
1import { createClient } from '@/lib/supabase/server'2import { NextRequest, NextResponse } from 'next/server'3import { z } from 'zod'45const schema = z.object({6 requestId: z.string().uuid(),7})89export async function POST(request: NextRequest) {10 const supabase = await createClient()11 const { data: { user } } = await supabase.auth.getUser()12 if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })1314 const body = await request.json()15 const { requestId } = schema.parse(body)1617 const { data, error } = await supabase.rpc('approve_leave', {18 request_id: requestId,19 approver_id: user.id,20 })2122 if (error) {23 if (error.message.includes('Insufficient leave balance')) {24 return NextResponse.json(25 { error: 'Insufficient leave balance' },26 { status: 422 }27 )28 }29 return NextResponse.json({ error: error.message }, { status: 500 })30 }3132 return NextResponse.json({ success: true })33}Customization ideas
Add payroll integration
Connect time entries to a payroll calculator that computes gross pay based on salary, overtime hours, and leave deductions.
Add performance reviews
Build a quarterly review system with rating forms, manager feedback, and goal tracking per employee.
Add org chart visualization
Render the department and manager hierarchy as an interactive tree chart using the parent_id relationships.
Add employee self-service portal
Let employees update their own contact info, view pay stubs, and download tax documents from a dedicated portal.
Add automated leave balance rollover
Build a Vercel Cron job that runs January 1st to calculate remaining vacation days and roll over a configurable percentage to the new year.
Common pitfalls
Pitfall: Approving leave without checking the balance atomically
How to avoid: Use a PostgreSQL RPC function with FOR UPDATE row locks that checks the balance and updates both tables in a single transaction.
Pitfall: Storing salary as a float instead of integer cents
How to avoid: Store salary as bigint in cents (salary_cents) and format for display client-side by dividing by 100.
Pitfall: Not restricting document access with Storage RLS
How to avoid: Configure Supabase Storage bucket RLS to restrict file reads to the employee themselves and users with the HR admin role.
Pitfall: Building the manager hierarchy without null checks
How to avoid: Make manager_id nullable and add null checks in approval workflows — requests from top-level employees go directly to HR admin.
Best practices
- Use a PostgreSQL RPC function for leave approval to enforce balance constraints at the database level, preventing race conditions.
- Store all monetary values (salary, budgets) as integer cents (bigint) to avoid floating-point rounding errors.
- Configure Supabase Storage bucket RLS to restrict document access to the employee and HR admins only.
- Connect to GitHub via V0's Git panel early — the multi-page HR system benefits from version-controlled incremental development.
- Seed leave_balances automatically when creating employees — use a PostgreSQL trigger or include balance creation in the onboarding Server Action.
- Use Clerk custom claims for role-based access (hr_admin, manager, employee) rather than building a custom role system.
- Validate all form inputs with Zod in Server Actions — enforce that leave end_date >= start_date and salary > 0.
AI prompts to try
Copy these prompts to build this project faster.
I'm building an HR management system with Next.js and Supabase. I need a PostgreSQL function that atomically approves a leave request: checks that the employee has sufficient leave balance, deducts the days, and updates the request status — all in one transaction with row-level locking. Show me the function SQL, how to call it via supabase.rpc(), and how to handle the insufficient balance error in the API route.
Build the leave approval queue for an HR system. Create a Server Component page that fetches pending leave requests with the employee name and leave balance. Each request row shows a Table with type, dates, days, reason, and Approve/Reject buttons. Approve calls a PostgreSQL RPC function that checks the balance and deducts days atomically. Reject opens an AlertDialog requiring a reason. Use Badge for status colors and Card for the balance summary.
Frequently asked questions
Can I build this with the free V0 plan?
The core pages fit within the free tier, but V0 Premium is recommended for the multi-page architecture. Use Design Mode (Option+D) for free visual tweaks across all plans.
How does the leave approval prevent over-spending balances?
A PostgreSQL RPC function uses FOR UPDATE row locks to atomically check the balance and deduct days in a single transaction. Even concurrent approvals cannot overdraw the balance.
What authentication should I use?
Clerk is recommended for the role-based access. Set custom claims (hr_admin, manager, employee) in the Clerk Dashboard metadata, then check roles in middleware.ts and Server Actions.
Can I add payroll features?
Yes. Use the time_entries and salary_cents data to calculate gross pay. For full payroll, integrate with a payroll API like Gusto or ADP through a Server Action.
How do I handle document storage?
Create a Supabase Storage bucket called employee-documents with RLS restricting access to the employee and HR admins. Upload via Server Action with FormData, store the file_url in the documents table.
How do I deploy?
Publish via V0's Share menu. Set SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, and Clerk credentials in the Vars tab. Configure the Storage bucket and RLS policies in Supabase Dashboard.
Can RapidDev help build a custom HR platform?
Yes. RapidDev has built 600+ apps including HR platforms with payroll integration, performance reviews, and compliance tracking. Book a free consultation to discuss your requirements.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation