# How to Build Attendance app with V0

- Tool: v0
- Difficulty: Intermediate
- Compatibility: V0 Premium or higher
- Last updated: April 2026

## TL;DR

Build a clock-in/clock-out attendance app with V0 using Next.js and Supabase. You'll get GPS-verified time tracking, a manager dashboard with department views, leave request management, and exportable CSV attendance reports — all in about 1-2 hours without any local setup.

## Before you start

- A V0 account (Premium plan recommended for multi-page projects)
- A Supabase project (free tier works — connect via V0's Connect panel)
- A Google Maps API key for location verification (optional but recommended)
- Basic understanding of your company's attendance and leave policies

## Step-by-step guide

### 1. Set up the project and database schema for attendance tracking

Create a new V0 project and connect Supabase via the Connect panel. Then prompt V0 to create the employee, attendance, and leave request tables with proper constraints and a partial unique index that prevents duplicate open clock-in sessions.

```
// Paste this prompt into V0's AI chat:
// Build an attendance tracking system with Supabase. Create these tables:
// 1. employees: id (uuid PK), user_id (uuid FK to auth.users), full_name (text), department (text), role (text check 'employee','manager'), is_active (boolean default true), created_at (timestamptz)
// 2. attendance_records: id (uuid PK), employee_id (uuid FK), clock_in (timestamptz), clock_out (timestamptz), clock_in_location (point), clock_out_location (point), total_hours (numeric generated always as extract(epoch from clock_out - clock_in)/3600), notes (text), status (text default 'present')
// 3. leave_requests: id (uuid PK), employee_id (uuid FK), leave_type (text), start_date (date), end_date (date), reason (text), status (text default 'pending'), approved_by (uuid FK)
// Add a partial unique index: CREATE UNIQUE INDEX ON attendance_records (employee_id) WHERE clock_out IS NULL
// Add RLS so employees see only their own records, managers see their department.
```

> Pro tip: The partial unique index is the key to preventing duplicate clock-ins. It ensures only one row per employee can have a NULL clock_out value at any time — if an employee tries to clock in twice, Supabase returns a constraint violation error.

**Expected result:** Supabase is connected with all tables created, RLS policies applied, and the partial unique index in place to prevent duplicate clock-in sessions.

### 2. Build the clock-in/clock-out interface with GPS capture

Create the employee-facing page with a large toggle button for clocking in and out. The component uses the browser's Geolocation API to capture coordinates and sends them to an API route that verifies the location server-side before recording the event.

```
import { NextRequest, NextResponse } from 'next/server'
import { createClient } from '@supabase/supabase-js'

const supabase = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!
)

export async function POST(req: NextRequest) {
  const { employeeId, action, latitude, longitude } = await req.json()
  const location = `(${longitude},${latitude})`

  if (action === 'clock_in') {
    const { data, error } = await supabase
      .from('attendance_records')
      .insert({
        employee_id: employeeId,
        clock_in: new Date().toISOString(),
        clock_in_location: location,
      })
      .select()
      .single()

    if (error?.code === '23505') {
      return NextResponse.json(
        { error: 'You already have an open clock-in session' },
        { status: 409 }
      )
    }
    if (error) return NextResponse.json({ error: error.message }, { status: 500 })
    return NextResponse.json({ data })
  }

  if (action === 'clock_out') {
    const { data, error } = await supabase
      .from('attendance_records')
      .update({
        clock_out: new Date().toISOString(),
        clock_out_location: location,
      })
      .eq('employee_id', employeeId)
      .is('clock_out', null)
      .select()
      .single()

    if (error) return NextResponse.json({ error: error.message }, { status: 500 })
    return NextResponse.json({ data })
  }

  return NextResponse.json({ error: 'Invalid action' }, { status: 400 })
}
```

**Expected result:** The API route handles clock-in and clock-out actions. Clock-in creates a new record with location; clock-out updates the open record. Duplicate clock-ins return a 409 conflict error.

### 3. Create the manager dashboard with department filtering

Build the manager-facing dashboard that shows today's attendance status for all department employees, weekly summaries, and the ability to filter by department. Use Server Components for efficient data fetching.

```
// Paste this prompt into V0's AI chat:
// Build a manager attendance dashboard at app/dashboard/page.tsx.
// Requirements:
// - Show today's attendance summary: Present count, Absent count, On Leave count in shadcn/ui Cards
// - Display a Table of today's attendance records with columns: Employee Name, Clock In Time, Clock Out Time, Total Hours, Status Badge
// - Add a Select dropdown to filter by department
// - Show a monthly Progress bar per employee showing attendance percentage (days present / working days)
// - Add Tabs for Today, This Week, This Month views
// - Weekly view shows a grid of days with green (present), red (absent), yellow (leave) indicators per employee
// - Use Server Components to fetch data from Supabase
// - Managers should only see employees in their department (enforce via Supabase RLS)
```

> Pro tip: Use Design Mode (Option+D) to adjust the dashboard Card layout, Progress bar colors, and attendance grid spacing without spending any credits.

**Expected result:** The manager dashboard shows today's attendance with department filtering, a weekly attendance grid, and monthly Progress bars per employee.

### 4. Build the leave request system with approval workflow

Create the leave request form for employees and the approval interface for managers. Employees submit requests with date ranges and reasons. Managers see pending requests and can approve or reject them with a single click.

```
'use server'

import { createClient } from '@supabase/supabase-js'
import { revalidatePath } from 'next/cache'

const supabase = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!
)

export async function submitLeaveRequest(
  employeeId: string,
  leaveType: string,
  startDate: string,
  endDate: string,
  reason: string
) {
  const { error } = await supabase.from('leave_requests').insert({
    employee_id: employeeId,
    leave_type: leaveType,
    start_date: startDate,
    end_date: endDate,
    reason,
  })

  if (error) throw new Error(error.message)
  revalidatePath('/leave')
}

export async function handleLeaveRequest(
  requestId: string,
  status: 'approved' | 'rejected',
  managerId: string
) {
  const { error } = await supabase
    .from('leave_requests')
    .update({ status, approved_by: managerId })
    .eq('id', requestId)
    .eq('status', 'pending')

  if (error) throw new Error(error.message)
  revalidatePath('/dashboard')
}
```

**Expected result:** Employees can submit leave requests with type, date range, and reason. Managers see pending requests and can approve or reject them, updating the status in Supabase.

### 5. Add exportable attendance reports

Create a reports page that generates monthly attendance summaries by department. Managers can view reports and export them as CSV files for payroll processing.

```
// Paste this prompt into V0's AI chat:
// Build an attendance reports page at app/reports/page.tsx.
// Requirements:
// - Select month and department using DatePicker and Select
// - Display a summary Table: Employee Name, Days Present, Days Absent, Days on Leave, Total Hours, Late Arrivals
// - Show department averages at the bottom
// - Add a "Download CSV" Button that generates and downloads a CSV file client-side
// - Add a PieChart (Recharts) showing the breakdown of present/absent/leave days for the department
// - Use Server Components for the initial data fetch
// - The CSV should include all columns plus clock-in and clock-out times for each day
```

**Expected result:** The reports page shows a monthly attendance summary table filterable by department with a downloadable CSV export and a PieChart showing attendance distribution.

## Complete code example

File: `app/api/attendance/route.ts`

```typescript
import { NextRequest, NextResponse } from 'next/server'
import { createClient } from '@supabase/supabase-js'

const supabase = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!
)

export async function POST(req: NextRequest) {
  const { employeeId, action, latitude, longitude } = await req.json()
  const location = `(${longitude},${latitude})`

  if (action === 'clock_in') {
    const { data, error } = await supabase
      .from('attendance_records')
      .insert({
        employee_id: employeeId,
        clock_in: new Date().toISOString(),
        clock_in_location: location,
      })
      .select()
      .single()

    if (error?.code === '23505') {
      return NextResponse.json(
        { error: 'Already clocked in' },
        { status: 409 }
      )
    }
    if (error) {
      return NextResponse.json({ error: error.message }, { status: 500 })
    }
    return NextResponse.json({ data })
  }

  if (action === 'clock_out') {
    const { data, error } = await supabase
      .from('attendance_records')
      .update({
        clock_out: new Date().toISOString(),
        clock_out_location: location,
      })
      .eq('employee_id', employeeId)
      .is('clock_out', null)
      .select()
      .single()

    if (error) {
      return NextResponse.json({ error: error.message }, { status: 500 })
    }
    return NextResponse.json({ data })
  }

  return NextResponse.json({ error: 'Invalid action' }, { status: 400 })
}

export async function GET(req: NextRequest) {
  const { searchParams } = new URL(req.url)
  const employeeId = searchParams.get('employee_id')

  const { data } = await supabase
    .from('attendance_records')
    .select('*')
    .eq('employee_id', employeeId!)
    .is('clock_out', null)
    .maybeSingle()

  return NextResponse.json({ active_session: data })
}
```

## Common mistakes

- **Storing GOOGLE_MAPS_API_KEY with NEXT_PUBLIC_ prefix** — The Google Maps API key should only be used server-side for location verification. Adding NEXT_PUBLIC_ exposes it in the browser bundle where anyone can steal it and rack up charges on your account. Fix: Store GOOGLE_MAPS_API_KEY in V0's Vars tab without any prefix. Proxy geolocation verification through your API route at app/api/attendance/route.ts.
- **Not preventing duplicate clock-in sessions** — Without a constraint, an employee could clock in multiple times without clocking out, creating overlapping sessions that corrupt total hours calculations. Fix: Add a partial unique index: CREATE UNIQUE INDEX ON attendance_records (employee_id) WHERE clock_out IS NULL. This guarantees only one open session exists per employee at the database level.
- **Calculating total hours on the client side** — Client-side time calculations can be manipulated by users or affected by timezone mismatches between the browser and server. Fix: Use a PostgreSQL generated column: total_hours numeric GENERATED ALWAYS AS (EXTRACT(EPOCH FROM clock_out - clock_in) / 3600). This calculates hours at the database level and cannot be tampered with.
- **Not handling timezone differences for distributed teams** — Storing times without timezone information causes clock-in records to appear at wrong times for employees in different timezones. Fix: Always use timestamptz (timestamp with time zone) in Supabase and store the employee's timezone in the employees table. Display times converted to the employee's local timezone on the frontend.

## Best practices

- Use a partial unique index on attendance_records to prevent duplicate open clock-in sessions at the database level
- Store GPS coordinates as Supabase point type for efficient location-based queries and geofencing
- Use Server Components for the manager dashboard to keep Supabase queries server-side and secure
- Store GOOGLE_MAPS_API_KEY in V0's Vars tab without NEXT_PUBLIC_ prefix and proxy verification through an API route
- Use Supabase RLS policies scoped by department so managers only see their own team's attendance records
- Use Design Mode (Option+D) to adjust the clock-in button size, dashboard layout, and status colors without spending credits
- Always use timestamptz columns to handle timezone differences correctly for distributed teams
- Use PostgreSQL generated columns for total_hours calculations to prevent client-side manipulation

## Frequently asked questions

### How does the GPS clock-in verification work?

When an employee taps the clock-in button, the browser's Geolocation API captures their coordinates. These are sent to the API route which can optionally verify them against your office location using the Google Maps API. The coordinates are stored with the attendance record for audit purposes.

### Can employees clock in from their phones?

Yes. The app is fully responsive and works in any mobile browser. The browser's Geolocation API works on both iOS and Android. For the best experience, employees should add the site to their home screen as a PWA.

### What V0 plan do I need for an attendance app?

V0 Premium is recommended because the attendance app requires multiple pages (clock-in, dashboard, reports, leave requests) and API routes. The free plan's limited credits may not cover the full build.

### How do I handle employees who forget to clock out?

Set up a Supabase Edge Function triggered by a cron schedule that runs at the end of each workday. It checks for open sessions (clock_out IS NULL) and either auto-clocks them out at the expected end time or flags them for manager review.

### How do I deploy the attendance app?

Click Share then Publish to Production in V0 for instant Vercel deployment. The app will be accessible from any browser. For custom domains, configure them in the Vercel Dashboard after publishing.

### Can I integrate this with payroll software?

Yes. Add an API route that exports attendance data in the format your payroll provider expects. Most payroll systems accept CSV imports, which you can generate from the monthly reports page.

### Can RapidDev help build a custom attendance system?

Yes. RapidDev has built 600+ apps including workforce management tools with advanced features like biometric verification, shift scheduling, and payroll integration. Book a free consultation to discuss your specific attendance tracking needs.

---

Source: https://www.rapidevelopers.com/how-to-build-v0/attendance-app
© RapidDev — https://www.rapidevelopers.com/how-to-build-v0/attendance-app
