# How to Build Habit tracker with V0

- Tool: v0
- Difficulty: Beginner
- Compatibility: V0 Free
- Last updated: April 2026

## TL;DR

Build a streak-based habit tracker with V0 using Next.js, Supabase for habit data, and a custom heatmap calendar showing daily completions. You'll create a today checklist, streak counters, and completion statistics — all in about 30-60 minutes without touching a terminal.

## Before you start

- A V0 account (free tier works for this project)
- A Supabase project (free tier works — connect via V0's Connect panel)
- Basic understanding of daily habits and streaks
- No coding experience needed — perfect for beginners

## Step-by-step guide

### 1. Set up the project and habit database schema

Open V0 and create a new project. Connect Supabase via the Connect panel. Create the schema for habits, completions, and streaks.

```
// Paste this prompt into V0's AI chat:
// Build a habit tracking app. Create a Supabase schema with:
// 1. habits: id (uuid PK), user_id (uuid FK to auth.users), name (text), description (text), icon (text), color (text default '#22c55e'), frequency (text check in 'daily','weekdays','weekly' default 'daily'), target_per_day (int default 1), position (int), is_archived (boolean default false), created_at (timestamptz)
// 2. completions: id (uuid PK), habit_id (uuid FK to habits), date (date), count (int default 1), created_at (timestamptz), unique(habit_id, date)
// 3. streaks: id (uuid PK), habit_id (uuid FK to habits), current_streak (int default 0), longest_streak (int default 0), last_completed_date (date)
// Add RLS: users see only their own habits and completions.
// Create a trigger that auto-creates a streaks row when a new habit is inserted.
```

> Pro tip: Use Design Mode (Option+D) to customize habit card colors and the heatmap palette — perfect for beginners since visual tweaks cost no credits.

**Expected result:** Database schema created with habits, completions, and streaks tables plus RLS policies restricting access to the authenticated user.

### 2. Build the today page with habit checklist

Create the main page showing today's habits as a checklist. Each habit shows its name, streak count, and a toggle to mark it complete. Include a weekly overview showing the last 7 days.

```
// Paste this prompt into V0's AI chat:
// Build a habit tracker homepage at app/page.tsx as a 'use client' component.
// Requirements:
// - Today's date prominently displayed at top
// - List of active habits as Card components, each with:
//   - Checkbox for completion toggle (optimistic UI)
//   - Habit name and icon
//   - Badge showing current streak with fire emoji
//   - Color indicator matching the habit's color
// - Weekly overview below: 7-day row per habit showing filled/empty circles
// - Overall completion rate for today (e.g., "4/6 habits done")
// - "Add Habit" Button linking to /habits
// - When Checkbox is toggled, call a Server Action that upserts into completions
//   and recalculates the streak in the streaks table
// - Use Card for each habit and Badge for streak count
```

**Expected result:** The homepage shows today's habits as cards with checkboxes, streak badges, and a weekly overview of the last 7 days.

### 3. Create the streak calculation Server Action

Build the Server Action that toggles a habit completion and recalculates the current streak. The streak logic counts consecutive days backwards from today.

```
// Paste this prompt into V0's AI chat:
// Build a Server Action at app/actions/habits.ts for toggling habit completion.
// Requirements:
// - toggleCompletion(habitId: string, date: string) Server Action
// - If completion exists for that habit+date, delete it. Otherwise upsert with count=1.
// - After toggling, recalculate the streak:
//   Query completions for this habit ordered by date DESC
//   Count consecutive days backwards from today (break on first missing day)
//   Update streaks table: current_streak = calculated value
//   If current_streak > longest_streak, update longest_streak too
// - Use Zod to validate habitId (uuid) and date (string)
// - revalidatePath('/') after the update
// - Also create addHabit and updateHabit Server Actions with Zod validation
```

> Pro tip: The streak recalculation runs in a single Server Action call, keeping the UI responsive with optimistic updates on the client side.

**Expected result:** Server Actions handle completion toggling with automatic streak recalculation, updating both the completions and streaks tables.

### 4. Build the completion heatmap calendar

Create the stats page with a GitHub-style heatmap calendar showing completion frequency over 365 days. Include per-habit streaks and overall statistics.

```
// Paste this prompt into V0's AI chat:
// Build a stats page at app/stats/page.tsx.
// Requirements:
// - Completion heatmap: a 365-day CSS grid (like GitHub contribution graph)
//   Each cell colored by completion count for that day:
//   0 = gray-100, 1 = green-200, 2 = green-400, 3+ = green-600
//   Wrap the heatmap in a 'use client' component, keep page as Server Component
// - Month labels above the heatmap grid
// - Streak leaderboard: Card for each habit showing current and longest streak
//   sorted by current streak DESC, with Badge for streak count
// - Overall stats Cards: total completions, current longest streak, best day, completion rate this month
// - Select to filter heatmap by specific habit or show all habits combined
// - Use Card for stat containers and Badge for streak indicators
```

**Expected result:** The stats page shows a 365-day heatmap calendar with green shading, per-habit streak rankings, and overall completion statistics.

### 5. Add habit management and archiving

Build the habit management page where users create, edit, reorder, and archive habits. Archived habits disappear from the today checklist but keep their history.

```
// Paste this prompt into V0's AI chat:
// Build a habits management page at app/habits/page.tsx.
// Requirements:
// - List all active habits in a reorderable list (drag handle icon)
// - Each habit row: icon, name, frequency Badge, color circle, edit Button, archive Switch
// - "Add Habit" Button opening Dialog with:
//   Input for name, Input for description, Select for icon (emoji picker),
//   color picker (6 preset color buttons), Select for frequency (daily/weekdays/weekly)
// - Edit: same Dialog pre-filled with existing values
// - Archive toggle: Switch that sets is_archived=true, habit disappears from today page
// - Archived section at bottom (collapsed by default) showing archived habits with restore Button
// - Server Actions for create, update, reorder (update position), and archive
// - Use Dialog for the form, Switch for archive toggle, Badge for frequency
```

**Expected result:** Users can create habits with custom names, icons, and colors, reorder them by dragging, and archive habits while preserving their completion history.

## Complete code example

File: `app/actions/habits.ts`

```typescript
'use server'

import { createClient } from '@/lib/supabase/server'
import { revalidatePath } from 'next/cache'
import { z } from 'zod'

const toggleSchema = z.object({
  habitId: z.string().uuid(),
  date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
})

export async function toggleCompletion(input: z.infer<typeof toggleSchema>) {
  const supabase = await createClient()
  const { data: { user } } = await supabase.auth.getUser()
  if (!user) throw new Error('Unauthorized')

  const { habitId, date } = toggleSchema.parse(input)

  const { data: existing } = await supabase
    .from('completions')
    .select('id')
    .eq('habit_id', habitId)
    .eq('date', date)
    .single()

  if (existing) {
    await supabase.from('completions').delete().eq('id', existing.id)
  } else {
    await supabase.from('completions').insert({ habit_id: habitId, date })
  }

  const { data: completions } = await supabase
    .from('completions')
    .select('date')
    .eq('habit_id', habitId)
    .order('date', { ascending: false })

  let streak = 0
  const today = new Date()
  for (let i = 0; i < 365; i++) {
    const checkDate = new Date(today)
    checkDate.setDate(today.getDate() - i)
    const dateStr = checkDate.toISOString().split('T')[0]
    if (completions?.some((c) => c.date === dateStr)) {
      streak++
    } else break
  }

  const { data: streakRow } = await supabase
    .from('streaks')
    .select('longest_streak')
    .eq('habit_id', habitId)
    .single()

  await supabase
    .from('streaks')
    .upsert({
      habit_id: habitId,
      current_streak: streak,
      longest_streak: Math.max(streak, streakRow?.longest_streak ?? 0),
      last_completed_date: existing ? null : date,
    }, { onConflict: 'habit_id' })

  revalidatePath('/')
  revalidatePath('/stats')
}
```

## Common mistakes

- **Not using optimistic UI for the completion toggle** — Waiting for the server round-trip before updating the checkbox feels laggy, especially on mobile. Fix: Toggle the checkbox state immediately on click, then call the Server Action. Revert if the action fails.
- **Recalculating streaks by scanning all completions on every page load** — As completion history grows, querying all rows on each visit wastes bandwidth and adds latency. Fix: Store the current streak in a dedicated streaks table and only recalculate when a completion is toggled.
- **Forgetting the unique constraint on completions(habit_id, date)** — Without it, rapid checkbox toggling can insert duplicate completions for the same day. Fix: Add a unique constraint on (habit_id, date) and use upsert instead of insert for completion toggling.

## Best practices

- Store streaks in a dedicated table updated on each toggle rather than computing from all completions on every page load.
- Use the unique constraint on completions(habit_id, date) to prevent duplicate entries from rapid toggling.
- Use Design Mode (Option+D) to customize habit card colors and heatmap shading for free — ideal for beginners.
- Implement optimistic UI for the completion checkbox so the toggle feels instant even with server-side validation.
- Use RLS policies with auth.uid() = user_id on all tables so each user sees only their own habits.
- Cache the heatmap data with ISR revalidation since historical completions don't change frequently.

## Frequently asked questions

### Can I build this with the free V0 plan?

Yes. The free tier provides enough credits for the core checklist, heatmap, and habit management pages. Use Design Mode (Option+D) for visual tweaks at no cost.

### How do streaks work?

A Server Action counts consecutive days backwards from today where a completion exists. The streak is stored in a dedicated streaks table and updated only when you toggle a habit, not on every page load.

### Can I track habits that aren't daily?

Yes. The frequency column supports daily, weekdays, and weekly. The streak calculation adjusts to only count applicable days based on the frequency setting.

### How does the heatmap calendar work?

A CSS grid renders 365 cells, one per day. Each cell queries the completions table for that date and is colored from gray (no completions) to dark green (3+ completions), similar to GitHub's contribution graph.

### Can I customize habit colors?

Yes. Each habit has a color field. The Card component uses this color for the left border accent, and the heatmap can filter by habit to show that habit's specific color.

### How do I deploy?

Publish via V0's Share menu. Set NEXT_PUBLIC_SUPABASE_ANON_KEY and NEXT_PUBLIC_SUPABASE_URL for client-side completion toggling, and SUPABASE_SERVICE_ROLE_KEY for Server Actions.

### Can RapidDev help build a custom habit tracking app?

Yes. RapidDev has built 600+ apps including habit and wellness platforms with social features, push notifications, and Apple Health integration. Book a free consultation to discuss your vision.

---

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