# How to Build HR management system with V0

- Tool: v0
- Difficulty: Advanced
- Compatibility: V0 Premium
- Last updated: April 2026

## TL;DR

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.

## Before you start

- 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)

## Step-by-step guide

### 1. 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.

```
// Paste this prompt into V0's AI chat:
// Build an HR management system. Create a Supabase schema with:
// 1. departments: id (uuid PK), name (text), head_id (uuid FK to employees nullable), parent_id (uuid FK to departments nullable), created_at (timestamptz)
// 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)
// 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)
// 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)
// 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)
// 6. time_entries: id (uuid PK), employee_id (uuid FK to employees), date (date), hours (decimal), project (text), notes (text), created_at (timestamptz)
// 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)
// 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.

### 2. Build the HR dashboard and employee directory

Create the main dashboard showing HR KPIs and the employee directory with search and filtering.

```
// Paste this prompt into V0's AI chat:
// Build an HR dashboard at app/page.tsx.
// Requirements:
// - Stats Cards row: total employees, open leave requests count, employees onboarding, departments count
// - Recharts BarChart showing headcount by department (wrap in 'use client' component)
// - PieChart showing leave type distribution for the current month
// - Recent activity feed: latest leave requests, new hires, completed onboarding tasks
// - Navigation to /employees, /leave, /reports
//
// Build employee directory at app/employees/page.tsx:
// - Table with columns: Avatar + name, department, job title, status Badge, start date, actions
// - Column sorting on name and start_date
// - Search Input filtering by name or email
// - Select filter for department
// - Badge colors: active=green, on_leave=yellow, terminated=red, onboarding=blue
// - Click row to navigate to /employees/[id]
// - "Add Employee" Button linking to /employees/new
```

**Expected result:** The dashboard shows headcount charts, leave distribution, and recent activity. The directory lists all employees with search, department filter, and status badges.

### 3. 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.

```
// Paste this prompt into V0's AI chat:
// Build employee profile at app/employees/[id]/page.tsx.
// Requirements:
// - Header: Avatar, full name, job title, department Badge, status Badge
// - Tabs component with sections:
//   1. Info tab: Card with personal details (email, phone, employment type, start date, salary), edit Button for HR admins
//   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 reason
//   3. Time tab: Table of time entries this month, total hours Card, "Log Time" Button
//   4. Documents tab: list of uploaded documents with download links, "Upload" Button using file Input that uploads to Supabase Storage bucket 'employee-documents'
//   5. Onboarding tab (visible only for status='onboarding'): Checkbox list of onboarding tasks with assigned person and due date
// - Server Actions for leave request submission, time entry logging, document upload, onboarding task completion
// - Zod validation on all form inputs
```

**Expected result:** Employee profile shows all HR data in organized tabs with forms for leave requests, time logging, document uploads, and onboarding task tracking.

### 4. 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.

```
// Paste this prompt into V0's AI chat:
// Build leave management at app/leave/page.tsx and create the approval RPC.
// Requirements:
// - Pending leave requests queue: Table showing employee name, leave type, dates, days requested, reason
// - Approve/Reject buttons per row
// - Create a PostgreSQL function approve_leave(request_id uuid, approver_id uuid):
//   BEGIN;
//   SELECT days, type, employee_id FROM leave_requests WHERE id = request_id AND status = 'pending' FOR UPDATE;
//   SELECT total_days - used_days AS remaining FROM leave_balances WHERE employee_id = emp AND type = leave_type AND year = current_year FOR UPDATE;
//   IF remaining < requested_days THEN RAISE EXCEPTION 'Insufficient leave balance';
//   UPDATE leave_balances SET used_days = used_days + requested_days;
//   UPDATE leave_requests SET status = 'approved', approved_by = approver_id;
//   COMMIT;
// - API route at app/api/leave/approve/route.ts that calls this RPC
// - Calendar overlay showing approved leave per team (visible to managers)
// - Badge for leave status: pending=yellow, approved=green, rejected=red
// - AlertDialog for rejection requiring a reason
```

> Pro 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.

### 5. Build multi-step onboarding and reports

Create the new employee onboarding form and the HR reports page with headcount trends and leave analytics.

```
// Paste this prompt into V0's AI chat:
// Build two pages:
// 1. app/employees/new/page.tsx — multi-step onboarding form ('use client'):
//    - Step 1: Personal info (first_name, last_name, email, phone, avatar upload)
//    - Step 2: Employment details (department Select, job_title, employment_type Select, start_date, salary)
//    - Step 3: Manager assignment (Select from existing employees in the chosen department)
//    - Step 4: Onboarding tasks (pre-filled checklist with common tasks, ability to add custom tasks)
//    - Progress indicator showing current step
//    - Server Action to insert employee + leave_balances (auto-create vacation/sick/personal with defaults) + onboarding_tasks in one transaction
//
// 2. app/reports/page.tsx — HR analytics:
//    - Recharts LineChart: headcount trend over 12 months
//    - BarChart: leave utilization by department
//    - PieChart: employment type distribution
//    - Cards: average tenure, turnover rate this year, departments with highest leave usage
//    - Select for time period filtering
//    - Wrap all charts in 'use client' components
```

**Expected 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 example

File: `app/api/leave/approve/route.ts`

```typescript
import { createClient } from '@/lib/supabase/server'
import { NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'

const schema = z.object({
  requestId: z.string().uuid(),
})

export async function POST(request: NextRequest) {
  const supabase = await createClient()
  const { data: { user } } = await supabase.auth.getUser()
  if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })

  const body = await request.json()
  const { requestId } = schema.parse(body)

  const { data, error } = await supabase.rpc('approve_leave', {
    request_id: requestId,
    approver_id: user.id,
  })

  if (error) {
    if (error.message.includes('Insufficient leave balance')) {
      return NextResponse.json(
        { error: 'Insufficient leave balance' },
        { status: 422 }
      )
    }
    return NextResponse.json({ error: error.message }, { status: 500 })
  }

  return NextResponse.json({ success: true })
}
```

## Common mistakes

- **Approving leave without checking the balance atomically** — Without a database-level transaction, two concurrent approvals can overdraw an employee's leave balance below zero. Fix: Use a PostgreSQL RPC function with FOR UPDATE row locks that checks the balance and updates both tables in a single transaction.
- **Storing salary as a float instead of integer cents** — Floating-point arithmetic causes rounding errors in financial calculations, leading to incorrect payroll figures. Fix: Store salary as bigint in cents (salary_cents) and format for display client-side by dividing by 100.
- **Not restricting document access with Storage RLS** — Without bucket-level RLS, any authenticated user can access another employee's contracts and personal documents. Fix: Configure Supabase Storage bucket RLS to restrict file reads to the employee themselves and users with the HR admin role.
- **Building the manager hierarchy without null checks** — The CEO or top-level employees have no manager, causing null reference errors in the approval chain. Fix: 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.

## 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.

---

Source: https://www.rapidevelopers.com/how-to-build-v0/hr-management-system
© RapidDev — https://www.rapidevelopers.com/how-to-build-v0/hr-management-system
