Build a comprehensive HR management system with Lovable and Supabase — covering employee directory, leave approvals, attendance tracking, and document storage. The multi-module layout uses Tabs, Supabase RLS for employee/manager/HR hierarchy, and Realtime notifications for the leave approval workflow. Expect 3–4 hours for a production-ready system.
What you're building
An HR management system in Lovable is organized around a single Tabs component at the top level, with each tab representing a distinct HR module: Directory, Leave, Attendance, Documents, and Analytics. All data lives in Supabase PostgreSQL across six core tables. The RLS hierarchy is the most critical design element: employees see only their own records, managers see records for all employees in their department, and HR administrators see everything.
The leave approval workflow demonstrates Supabase Realtime in action. When an employee submits a leave request, a row is inserted into leave_requests with status = 'pending'. Lovable subscribes the manager's browser to changes on that table filtered by their team. The manager sees a notification badge appear in the Leave tab, opens the request, and clicks Approve or Reject. The status update triggers another Realtime event that notifies the requesting employee in their open browser tab — all without email or a separate notification service.
Document storage uses Supabase Storage private buckets. Files are uploaded through the Lovable UI, stored per-employee in a structured path (employee-docs/employee_id/filename), and accessed via time-limited signed URLs. HR staff can view documents for any employee; employees can only view their own.
Final result
A fully functional HR system with multi-tier access control, leave approvals with live notifications, attendance tracking, and secure document storage — deployed from Lovable.
Tech stack
Prerequisites
- Lovable Business plan recommended due to the volume of steps and credit usage in advanced builds
- Supabase project with Storage enabled (free tier works for development)
- Supabase URL, anon key, and service role key ready for Cloud tab → Secrets
- Org structure ready: list of departments and position titles
- At least one test employee and one test manager email address for testing the approval workflow
- Familiarity with Supabase RLS concepts (policies, auth.uid(), joins)
Build steps
Generate the full HR schema with RLS hierarchy
This is the most complex schema prompt in this guide. Take time to review the generated SQL before proceeding. The RLS policies for the manager tier require a subquery joining employees to departments — ask Lovable to generate and validate these policies explicitly.
1Create an HR management system with Supabase. Set up these tables:23- departments: id, org_id, name, manager_id (references employees.id, nullable), created_at4- positions: id, org_id, department_id, title, level (junior|mid|senior|lead|manager|director|executive), created_at5- employees: id, org_id, user_id (references auth.users, nullable), first_name, last_name, email, phone, department_id, position_id, manager_id (self-referencing FK to employees.id), hire_date, employment_type (full_time|part_time|contractor), status (active|on_leave|terminated), avatar_url, created_at6- leave_requests: id, employee_id, type (annual|sick|maternity|paternity|unpaid|other), start_date, end_date, days_count (computed), reason, status (pending|approved|rejected|cancelled), reviewed_by (references employees.id), review_note, created_at, updated_at7- attendance: id, employee_id, date, check_in_at, check_out_at, work_hours (computed), status (present|absent|late|half_day), notes, created_at8- employee_documents: id, employee_id, name, type (contract|id|certificate|other), storage_path, uploaded_by, created_at910RLS policies:11- Employee tier: employees.SELECT WHERE user_id = auth.uid(), leave_requests.SELECT WHERE employee_id = (SELECT id FROM employees WHERE user_id = auth.uid()), attendance.SELECT WHERE employee_id = same12- Manager tier: employees.SELECT WHERE department_id IN (SELECT department_id FROM employees WHERE user_id = auth.uid()), same scope for leave_requests and attendance13- HR tier: full SELECT/INSERT/UPDATE/DELETE on all tables WHERE org_id matches14- Implement role detection via a profiles table: id, user_id, org_id, hr_role (employee|manager|hr_admin)Pro tip: Use Lovable's Plan Mode first for this step. The schema is complex enough that having Lovable reason through the RLS logic before generating code reduces the chance of policy errors.
Expected result: All six tables plus departments, positions, and profiles are created. TypeScript types are generated. RLS is enabled on all tables. The preview shows a basic app shell.
Build the main Tabs layout and employee directory
Ask Lovable to create the top-level Tabs navigation and the Directory tab first. The employee directory is the most-used module and makes a good visual foundation for the rest of the app.
1Build the main HR app layout with a Tabs navigation at src/pages/HR.tsx.23Tabs structure:4- Directory: employee directory5- Leave: leave requests management6- Attendance: attendance tracking7- Documents: document storage8- Analytics: charts and reports910For the Directory tab:11- Fetch employees with joins on departments and positions12- Render as a shadcn/ui DataTable (TanStack Table v8)13- Columns: Avatar + full name (clickable), position title, department name, employment type Badge (full_time=blue, part_time=yellow, contractor=gray), status Badge (active=green, on_leave=orange, terminated=red), hire date, manager name14- Add a department filter Select above the table15- Add a search Input that filters by name or email16- Clicking a row opens an employee profile Sheet showing all details, their manager, and quick links to their leave history and documents17- HR admins see an Edit button in the Sheet; employees see read-only view of their own profilePro tip: Ask Lovable to use a nested select in the Supabase query: employees.select('*, department:departments(name), position:positions(title), manager:employees!manager_id(first_name, last_name)') to get all related names in a single query.
Expected result: The HR app shows a Tabs layout. The Directory tab renders a filterable employee table with Avatars, Badges, and working search. Clicking a row opens the profile Sheet.
Build the leave request workflow with Realtime notifications
The leave module is the most complex feature: employees submit requests, managers see pending requests in their queue, and approval or rejection triggers a notification in the requester's browser. Build this step by step.
1Build the Leave tab module. It should show different UIs based on the user's hr_role:23For employees (hr_role = 'employee'):4- Show their own leave history as a DataTable: type Badge, dates, days count, status Badge (pending=yellow, approved=green, rejected=red, cancelled=gray), review note if rejected5- 'Request Leave' Button that opens a Dialog6- Dialog form (react-hook-form + zod): leave type Select, start date Calendar Popover, end date Calendar Popover (auto-calculates days_count excluding weekends), reason Textarea7- On submit, insert into leave_requests with status = 'pending'8- Subscribe to Realtime on leave_requests WHERE id = request.id — when status changes to approved or rejected, show a Sonner toast: 'Your leave request was approved' or 'Your leave request was rejected: [review_note]'910For managers (hr_role = 'manager'):11- Show a DataTable of pending leave requests from their team12- Each row has Approve Button (green) and Reject Button (red)13- Reject opens a small Dialog to enter a review_note14- On approve/reject, update leave_requests with status and reviewed_by15- Show a Badge count on the Leave tab label showing total pending requests1617For HR admins: show all requests with ability to filter by department and export as CSVPro tip: Ask Lovable to add a leave balance summary card above the employee's request history showing total annual leave days, used days, and remaining days — calculated from approved leave requests for the current year.
Expected result: Employees can submit leave requests. Managers see pending requests with approve/reject actions. The requesting employee's browser shows a toast when the status changes.
Build the attendance tracking module
Ask Lovable to build the attendance tab with check-in/check-out functionality and a monthly calendar view. The attendance data feeds the Analytics charts built in the next step.
1import { useState } from 'react'2import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'3import { supabase } from '@/integrations/supabase/client'4import { Button } from '@/components/ui/button'5import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'6import { Badge } from '@/components/ui/badge'7import { Calendar } from '@/components/ui/calendar'8import { format, isToday, parseISO } from 'date-fns'910type AttendanceRecord = {11 id: string12 date: string13 check_in_at: string | null14 check_out_at: string | null15 work_hours: number | null16 status: 'present' | 'absent' | 'late' | 'half_day'17}1819export function AttendanceTab({ employeeId }: { employeeId: string }) {20 const [month, setMonth] = useState(new Date())21 const queryClient = useQueryClient()2223 const { data: records = [] } = useQuery({24 queryKey: ['attendance', employeeId, format(month, 'yyyy-MM')],25 queryFn: async () => {26 const start = format(new Date(month.getFullYear(), month.getMonth(), 1), 'yyyy-MM-dd')27 const end = format(new Date(month.getFullYear(), month.getMonth() + 1, 0), 'yyyy-MM-dd')28 const { data } = await supabase29 .from('attendance')30 .select('*')31 .eq('employee_id', employeeId)32 .gte('date', start)33 .lte('date', end)34 .order('date', { ascending: false })35 return (data ?? []) as AttendanceRecord[]36 },37 })3839 const todayRecord = records.find((r) => isToday(parseISO(r.date)))4041 const checkIn = useMutation({42 mutationFn: async () => {43 const now = new Date().toISOString()44 const hour = new Date().getHours()45 const status = hour > 9 ? 'late' : 'present'46 await supabase.from('attendance').upsert({47 employee_id: employeeId,48 date: format(new Date(), 'yyyy-MM-dd'),49 check_in_at: now,50 status,51 })52 },53 onSuccess: () => queryClient.invalidateQueries({ queryKey: ['attendance'] }),54 })5556 const checkOut = useMutation({57 mutationFn: async () => {58 await supabase59 .from('attendance')60 .update({ check_out_at: new Date().toISOString() })61 .eq('id', todayRecord!.id)62 },63 onSuccess: () => queryClient.invalidateQueries({ queryKey: ['attendance'] }),64 })6566 const presentDates = records.filter((r) => r.status === 'present' || r.status === 'late').map((r) => parseISO(r.date))6768 return (69 <div className="grid grid-cols-1 md:grid-cols-3 gap-4">70 <Card>71 <CardHeader><CardTitle className="text-base">Today</CardTitle></CardHeader>72 <CardContent className="space-y-3">73 {todayRecord?.check_in_at ? (74 <>75 <p className="text-sm">Checked in: {format(parseISO(todayRecord.check_in_at), 'h:mm a')}</p>76 {todayRecord.check_out_at77 ? <p className="text-sm">Checked out: {format(parseISO(todayRecord.check_out_at), 'h:mm a')}</p>78 : <Button onClick={() => checkOut.mutate()} variant="outline" className="w-full">Check Out</Button>79 }80 </>81 ) : (82 <Button onClick={() => checkIn.mutate()} className="w-full">Check In</Button>83 )}84 {todayRecord && <Badge variant="outline">{todayRecord.status}</Badge>}85 </CardContent>86 </Card>87 <Card className="md:col-span-2">88 <CardHeader><CardTitle className="text-base">Monthly View</CardTitle></CardHeader>89 <CardContent>90 <Calendar mode="multiple" selected={presentDates} month={month} onMonthChange={setMonth} className="rounded-md" />91 </CardContent>92 </Card>93 </div>94 )95}Expected result: Employees can check in and check out from the Attendance tab. The calendar shows days worked highlighted. Late arrivals are automatically tagged.
Build secure document storage and the analytics dashboard
Ask Lovable to set up Supabase Storage for employee documents with per-employee access control, and build the Analytics tab with Recharts charts showing headcount and leave trends.
1Add two final modules:231. Documents tab:4- Create a Supabase Storage bucket called 'employee-docs' set to PRIVATE5- Storage path pattern: employee-docs/{employee_id}/{filename}6- Storage RLS: employees can upload/download only paths starting with their employee_id. HR admins can access all paths.7- Build a document list component showing files per employee: name, type Badge, upload date, and a Download button8- Download button calls supabase.storage.from('employee-docs').createSignedUrl(path, 3600) and opens the URL in a new tab9- Add a file upload Input (accept pdf, jpg, png, docx) that uploads to the correct path and inserts a row into employee_documents10- HR admins see a Select to choose which employee's documents they are viewing11122. Analytics tab (HR admins only):13- Headcount by department: fetch COUNT(*) from employees grouped by department_id, joined to departments.name. Render as a Recharts BarChart.14- Leave utilization: fetch approved leave_requests, group by type, sum days_count. Render as a Recharts PieChart.15- Attendance rate this month: (present + late count) / total working days. Show as a big percentage number with a Progress bar.16- Recent hires: DataTable of last 10 employees ordered by hire_date desc.Pro tip: For the storage RLS, ask Lovable to create a storage policy in Supabase using the dashboard path: Storage → Policies → New Policy. The condition is: (storage.foldername(name))[1] = (SELECT id::text FROM employees WHERE user_id = auth.uid()).
Expected result: Employees can upload and download their own documents. HR admins see documents for any employee. The Analytics tab renders department headcount and leave type charts.
Complete code
1import { useForm } from 'react-hook-form'2import { zodResolver } from '@hookform/resolvers/zod'3import { z } from 'zod'4import { addDays, differenceInBusinessDays, format } from 'date-fns'5import { supabase } from '@/integrations/supabase/client'6import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'7import { Button } from '@/components/ui/button'8import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'9import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'10import { Textarea } from '@/components/ui/textarea'11import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'12import { Calendar } from '@/components/ui/calendar'13import { CalendarIcon } from 'lucide-react'14import { toast } from 'sonner'1516const TYPES = ['annual','sick','maternity','paternity','unpaid','other'] as const17const schema = z.object({18 type: z.enum(TYPES), reason: z.string().min(10),19 start_date: z.date({ required_error: 'Start date required' }),20 end_date: z.date({ required_error: 'End date required' }),21}).refine(d => d.end_date >= d.start_date, { message: 'End after start', path: ['end_date'] })2223type F = z.infer<typeof schema>24type Props = { open: boolean; onClose: () => void; employeeId: string }2526export function LeaveRequestDialog({ open, onClose, employeeId }: Props) {27 const form = useForm<F>({ resolver: zodResolver(schema), defaultValues: { type: 'annual', reason: '' } })28 const [s, e] = [form.watch('start_date'), form.watch('end_date')]29 const days = s && e ? differenceInBusinessDays(addDays(e, 1), s) : 03031 async function onSubmit(v: F) {32 const { error } = await supabase.from('leave_requests').insert({33 employee_id: employeeId, type: v.type, reason: v.reason, days_count: days, status: 'pending',34 start_date: format(v.start_date, 'yyyy-MM-dd'), end_date: format(v.end_date, 'yyyy-MM-dd'),35 })36 if (error) { toast.error('Failed to submit'); return }37 toast.success('Leave request submitted'); form.reset(); onClose()38 }3940 const DatePicker = (name: 'start_date' | 'end_date', label: string) => (41 <FormField control={form.control} name={name} render={({ field }) => (42 <FormItem><FormLabel>{label}</FormLabel>43 <Popover><PopoverTrigger asChild><FormControl>44 <Button variant="outline" className="w-full justify-start">45 <CalendarIcon className="mr-2 h-4 w-4" />{field.value ? format(field.value, 'MMM d') : 'Pick date'}46 </Button>47 </FormControl></PopoverTrigger>48 <PopoverContent className="w-auto p-0">49 <Calendar mode="single" selected={field.value} onSelect={field.onChange}50 disabled={d => d < (name === 'end_date' ? s ?? new Date() : new Date())} />51 </PopoverContent>52 </Popover><FormMessage /></FormItem>53 )} />54 )5556 return (57 <Dialog open={open} onOpenChange={onClose}>58 <DialogContent className="max-w-md">59 <DialogHeader><DialogTitle>Request Leave</DialogTitle></DialogHeader>60 <Form {...form}><form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">61 <FormField control={form.control} name="type" render={({ field }) => (62 <FormItem><FormLabel>Leave Type</FormLabel>63 <Select onValueChange={field.onChange} defaultValue={field.value}>64 <FormControl><SelectTrigger><SelectValue /></SelectTrigger></FormControl>65 <SelectContent>{TYPES.map(t => <SelectItem key={t} value={t}>{t[0].toUpperCase()+t.slice(1)}</SelectItem>)}</SelectContent>66 </Select><FormMessage /></FormItem>67 )} />68 <div className="grid grid-cols-2 gap-3">{DatePicker('start_date','Start')}{DatePicker('end_date','End')}</div>69 {days > 0 && <p className="text-sm text-muted-foreground">{days} business day{days !== 1 ? 's' : ''}</p>}70 <FormField control={form.control} name="reason" render={({ field }) => (71 <FormItem><FormLabel>Reason</FormLabel>72 <FormControl><Textarea placeholder="Brief reason..." {...field} /></FormControl>73 <FormMessage /></FormItem>74 )} />75 <Button type="submit" className="w-full" disabled={form.formState.isSubmitting}>Submit Request</Button>76 </form></Form>77 </DialogContent>78 </Dialog>79 )80}Customization ideas
Payroll export integration
Add a payroll report generator that aggregates attendance (work_hours), leave (days_count by type), and department data into a downloadable CSV. HR can filter by pay period and department. Run the aggregation as a Supabase RPC function and export from the Analytics tab.
Performance review cycle
Add a performance_reviews table with rating criteria, goal tracking, and a review form. Managers submit reviews for their direct reports each quarter. Employees can self-assess before the manager review. A review completion dashboard shows HR who has and hasn't completed their cycle.
Onboarding checklist
Add an onboarding_tasks table with predefined task templates per department. When a new employee's hire_date arrives, auto-generate their task list via a Supabase Edge Function. The HR module shows a checklist interface where managers can check off tasks as the new hire completes orientation.
Organizational chart visualization
Add an Org Chart page that visualizes the manager_id hierarchy in the employees table. Use a tree-layout library compatible with React to render each employee as an Avatar Card connected by lines. Clicking a card opens the employee profile Sheet.
Shift scheduling module
Add a shifts table for businesses with non-standard hours. HR creates shift templates (Morning 8am–4pm, Evening 4pm–midnight). Managers assign shifts to employees per week using a Calendar-based grid. Employees see their schedule on their dashboard and can request shift swaps via a workflow similar to leave approvals.
Common pitfalls
Pitfall: Using the same RLS policy for managers and HR without distinguishing access scope
How to avoid: The manager SELECT policy on employees must include: department_id IN (SELECT department_id FROM employees WHERE user_id = auth.uid()). Test by logging in as a manager and trying to query an employee in a different department — it should return zero rows.
Pitfall: Storing private documents in a public Supabase Storage bucket
How to avoid: Create the storage bucket as PRIVATE. Use supabase.storage.createSignedUrl(path, expirySeconds) to generate time-limited download URLs. Set expiry to 3600 seconds (1 hour) maximum for sensitive documents.
Pitfall: Not adding cleanup to the leave request Realtime subscription
How to avoid: Return a cleanup from useEffect: return () => { supabase.removeChannel(channel) }. Scope the subscription to the specific leave request ID using .filter('id=eq.' + requestId) to avoid receiving events for other employees.
Pitfall: Calculating work_hours client-side instead of in the database
How to avoid: Add a PostgreSQL generated column: work_hours NUMERIC GENERATED ALWAYS AS (EXTRACT(EPOCH FROM (check_out_at - check_in_at)) / 3600) STORED. This calculation is automatic and cannot be overridden from the client.
Pitfall: Building all five modules at once in a single Lovable prompt
How to avoid: Build one module per prompt, verify it works in the preview, then move to the next. Use Lovable's Plan Mode to review the approach for complex modules like the leave workflow before generating code.
Best practices
- Design the RLS hierarchy before writing any application code. Draw the access matrix on paper: rows = tables, columns = roles. Fill in which operations each role can perform. Then translate each cell to a Supabase policy.
- Use Supabase's generate column feature for computed fields like work_hours and days_count instead of calculating in application code. Generated columns are always consistent and reduce the risk of bugs.
- Limit Realtime subscriptions to specific rows using filters. For the leave approval workflow, each manager subscribes to leave_requests WHERE department_id IN (...) rather than the entire table. This reduces unnecessary traffic.
- Store document metadata (name, type, path, uploader) in the employee_documents table and the actual file in Supabase Storage. Never store files as base64 in PostgreSQL — it destroys query performance.
- Build a separate admin-only seed page that lets HR managers import employees from a CSV. This is far more practical than entering 50+ employees manually via form. Ask Lovable to add a Papa Parse-powered CSV importer.
- Add soft-delete (status = 'terminated' + terminated_at timestamp) instead of DELETE for employees. Employment records have legal retention requirements in many jurisdictions.
- Test the three-tier RLS by creating three Supabase Auth users with different roles and running queries in the Supabase SQL editor as each user using SET LOCAL role = ... to simulate the auth context.
AI prompts to try
Copy these prompts to build this project faster.
I'm building an HR management system in Lovable (React + Supabase). I have a three-tier access hierarchy: employees see their own data, managers see their department, HR admins see everything. The employees table has: id, user_id, department_id, manager_id. Write the complete Supabase RLS SQL policies for all three tiers on the employees, leave_requests, and attendance tables. For the manager tier, the policy must use a subquery to find the manager's department_id from the employees table. Include CREATE POLICY statements I can run directly in the Supabase SQL editor.
Add a leave balance calculator to the Leave tab. For the currently logged-in employee, calculate: (1) total annual leave days allowed per year based on their position level (junior=15, mid=18, senior=20, lead=22, manager=25), (2) days used this calendar year (sum of days_count from approved annual leave_requests where EXTRACT(year FROM start_date) = current year), (3) days remaining. Show these as three metric Cards at the top of the Leave tab. Update in real time when a new leave request is approved.
In Supabase, write a PostgreSQL function called notify_manager_on_leave_request() that runs as a trigger AFTER INSERT on leave_requests. The function should: 1) Find the manager_id of the requesting employee from the employees table. 2) Find the manager's user_id from employees. 3) Insert a row into a notifications table: (user_id = manager user_id, type = 'leave_request', title = 'New leave request from [employee name]', body = '[days] days from [start] to [end]', link = '/hr?tab=leave&id=[request_id]', is_read = false). This enables the Realtime notification badge without a separate service.
Frequently asked questions
How do I import existing employee data from a spreadsheet?
Ask Lovable to add a CSV import feature to the Directory tab. It will generate a file input, Papa Parse integration to read the CSV, a column-mapping step, a preview DataTable, and a batch insert into the employees table. For large imports (500+ employees), ask Lovable to use a Supabase Edge Function for the insert so the browser doesn't time out.
Can I use this system with employees in multiple countries?
Yes. Add a country column to employees and a public_holidays table with (country, date, name). Update the leave days_count calculation to exclude country-specific public holidays in addition to weekends. The Supabase RPC function for calculating business days between two dates can take country as a parameter.
How do I make the leave approval notification appear even if the manager has the app closed?
Supabase Realtime only delivers to connected browsers. For push notifications when the app is closed, add a Supabase Database Webhook on leave_requests INSERT that triggers an Edge Function. The function sends an email via Resend to the manager's email address. Store the Resend API key in Cloud tab → Secrets.
Is this secure enough for real employee data?
With correctly configured RLS, Supabase provides strong row-level isolation. For production HR systems, also: enable Supabase's Point-in-Time Recovery (Pro plan), set up Storage private buckets for documents, enable MFA for HR admin accounts via Supabase Auth, and add audit logging (a trigger that records every UPDATE on sensitive tables). Do not store sensitive fields like salary in plain text — encrypt them at the application layer.
Can I test the three-tier access control in Lovable?
Create three separate user accounts in your Supabase Auth dashboard with emails like employee@test.com, manager@test.com, and hradmin@test.com. Set their hr_role in the profiles table accordingly. Log in to your Lovable app as each user in separate browser tabs (or incognito windows) and verify that each role sees only the data they should access.
How do I add a payroll export feature?
Ask Lovable to add a Payroll Export button in the Analytics tab (visible to HR admins only). The button calls a Supabase RPC function that aggregates attendance hours, leave days, and employee details for a selected pay period. The function returns a JSON array which Lovable converts to CSV using PapaParse and triggers a browser download with the correct Content-Disposition header.
Is there a team that can help build a custom HR system in Lovable?
RapidDev builds production Lovable apps including HR systems with custom approval workflows, payroll integrations, and compliance-grade access control. Get in touch if your requirements go beyond this guide.
How do I deploy the HR system so my team can access it?
Click the Publish icon in the top-right corner of Lovable to deploy to a public URL. For a custom company domain (e.g. hr.yourcompany.com), go to Publish → Settings → Custom Domain on a paid Lovable plan. Make sure Supabase RLS is configured before sharing the URL — without it, all employee data is visible to every authenticated user.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation