# How to Build a Habit Tracker with Lovable

- Tool: How to Build with Lovable
- Difficulty: Beginner
- Compatibility: Lovable free tier or higher
- Last updated: April 2026

## TL;DR

Build a daily habit tracker in Lovable with streak calculation via a Postgres function, a GitHub-style calendar heatmap, one-click completion toggle, and weekly and monthly Recharts charts — all backed by Supabase with per-user data isolation and an encouraging dashboard that makes consistent habits feel rewarding.

## Before you start

- Lovable account (free tier works for this entire build)
- Supabase project with SUPABASE_URL and SUPABASE_ANON_KEY available
- A list of 3-5 habits you want to track as test data
- Basic understanding of how habits and streaks work

## Step-by-step guide

### 1. Create the habit tracking schema and streak function

Ask Lovable to create the two core tables and the Postgres streak calculation function. The function is the most complex part — everything else is straightforward CRUD.

```
Create a habit tracking schema in Supabase.

Tables:
- habits: id, user_id (references auth.users), name, description, color (text, hex color like '#6366f1'), icon (text, emoji or icon name), frequency ('daily' | 'weekdays' | 'custom'), custom_days (int array, 0=Sunday to 6=Saturday, nullable), is_archived (bool default false), created_at
- habit_completions: id, habit_id (references habits), user_id (references auth.users), completed_date (date), notes (text, nullable), created_at

Unique constraint on habit_completions(habit_id, completed_date) to prevent duplicate completions for the same day.

Create a Postgres function calculate_streaks(p_habit_id uuid, p_user_id uuid) that returns (current_streak int, longest_streak int):
- Get all completed_date values for this habit, ordered DESC
- Current streak: count consecutive days starting from today (or yesterday if today not yet completed). Stop counting when there is a gap of more than 1 day.
- Longest streak: find the maximum run of consecutive dates in the full history.
- Return both values.

RLS:
- habits: users SELECT/INSERT/UPDATE/DELETE their own (user_id = auth.uid())
- habit_completions: users SELECT/INSERT/DELETE their own rows

Create an index on habit_completions(habit_id, completed_date DESC) and (user_id, completed_date DESC).
```

> Pro tip: The streak function can be simplified using a window function. Ask Lovable to use: SELECT completed_date, completed_date - ROW_NUMBER() OVER (ORDER BY completed_date)::int as grp FROM habit_completions WHERE habit_id = p_habit_id. Dates in the same consecutive run have the same grp value. Then GROUP BY grp and COUNT to get streak lengths.

**Expected result:** Both tables are created with the unique constraint. The calculate_streaks function is callable via supabase.rpc(). TypeScript types are generated.

### 2. Build the habits dashboard with completion toggles

The main page users see every day: their habits for today with checkboxes, current streaks, and a quick view of what's done.

```
Build a habits dashboard at src/pages/Dashboard.tsx.

Requirements:
- Fetch all non-archived habits for the current user
- For each habit, check if a habit_completions row exists for today's date
- Display habits as a list of Cards. Each Card shows:
  - A large Checkbox (or Toggle) on the left. Clicking it creates or deletes a habit_completions row for today.
  - Habit name, description (truncated), and color indicator stripe on the left edge of the card
  - Current streak displayed as a flame icon + number (e.g. '🔥 7 days'). Fetch this from calculate_streaks via supabase.rpc().
  - Completed cards have a green background tint and the checkbox is checked
  - A menu (three-dot icon) with options: Edit, Archive, View Stats
- Use optimistic updates: clicking the toggle updates local state immediately. If the Supabase call fails, revert and show a Toast error.
- Progress bar at the top: 'X of Y habits done today' with percentage
- 'Add Habit' Button opening a Sheet with the habit creation form
```

**Expected result:** The dashboard shows today's habits. Clicking a toggle instantly checks it off with visual feedback. The streak counter shows next to each habit. The progress bar updates as habits are completed.

### 3. Add the calendar heatmap

Ask Lovable to build the GitHub-style contribution heatmap showing 12 weeks of habit completion history.

```
Add a calendar heatmap component at src/components/HabitHeatmap.tsx.

Requirements:
- Accept props: completionsByDate (Record<string, number>) where key is 'YYYY-MM-DD' and value is count of habits completed that day
- Render a 12-week grid (84 day squares) from 83 days ago to today, arranged as 7 rows (Mon-Sun) x 12 columns (weeks)
- Square colors: 0 completions = bg-gray-100 (dark mode: bg-gray-800), 1-2 = bg-green-200, 3-4 = bg-green-400, 5+ = bg-green-600
- Each square is a Tooltip-wrapped div showing the date and completion count on hover
- Show month labels above the grid where the month changes
- Show day labels (M, W, F) on the left side
- Query for the heatmap data: SELECT completed_date, COUNT(*) as count FROM habit_completions WHERE user_id = auth.uid() AND completed_date >= (current_date - 83) GROUP BY completed_date
- Render this component on the Dashboard page below the habit list
```

**Expected result:** The heatmap renders 84 day squares in the correct calendar layout. Squares with more completions are darker green. Hovering any square shows the date and count. Days with no data show as empty gray.

### 4. Build the stats page with charts

A dedicated stats page gives users deeper insight into their consistency. Ask Lovable to build it with Recharts.

```
Build a stats page at src/pages/Stats.tsx.

Requirements:
- Habit selector at the top: a Select component listing all habits. Default = 'All habits'.
- When a specific habit is selected, show:
  - Stat Cards: Current Streak, Longest Streak, Total Completions, Completion Rate (completions / days since habit was created)
  - LineChart: completion rate per week (percentage, last 12 weeks)
  - Call calculate_streaks via supabase.rpc() for the streak data
- When 'All habits' is selected, show:
  - Stat Cards: Total habits, Habits completed today, Perfect days (days where all active habits were completed), Best day (date with most completions)
  - BarChart: per-habit completion count for the last 30 days, one bar per habit, colored with the habit's color
  - The heatmap component from Step 3
- All charts use Recharts with custom tooltips showing formatted date and value
- Add a month/year selector to change the time period for charts
```

**Expected result:** The stats page shows correct data for both all-habits and per-habit views. The bar chart uses each habit's color. Streak stats match what the dashboard shows. The time period selector updates all charts.

## Complete code example

File: `src/hooks/useHabitToggle.ts`

```typescript
import { useState } from 'react'
import { supabase } from '@/integrations/supabase/client'
import { useToast } from '@/components/ui/use-toast'

type HabitCompletion = {
  id: string
  habit_id: string
  completed_date: string
}

export function useHabitToggle(
  habitId: string,
  initialCompletion: HabitCompletion | null
) {
  const [completion, setCompletion] = useState<HabitCompletion | null>(initialCompletion)
  const [isLoading, setIsLoading] = useState(false)
  const { toast } = useToast()

  const today = new Date().toISOString().split('T')[0]

  const toggle = async () => {
    if (isLoading) return
    setIsLoading(true)

    const previousCompletion = completion

    if (completion) {
      // Optimistic: mark incomplete
      setCompletion(null)
      const { error } = await supabase
        .from('habit_completions')
        .delete()
        .eq('id', completion.id)

      if (error) {
        setCompletion(previousCompletion)
        toast({ title: 'Could not update habit', variant: 'destructive' })
      }
    } else {
      // Optimistic: mark complete with a temporary id
      const tempCompletion = { id: 'temp', habit_id: habitId, completed_date: today }
      setCompletion(tempCompletion)

      const { data, error } = await supabase
        .from('habit_completions')
        .insert({ habit_id: habitId, completed_date: today })
        .select()
        .single()

      if (error) {
        setCompletion(null)
        toast({ title: 'Could not update habit', variant: 'destructive' })
      } else if (data) {
        setCompletion(data)
      }
    }

    setIsLoading(false)
  }

  return { isCompleted: !!completion, toggle, isLoading }
}
```

## Common mistakes

- **Using a timestamp instead of a date type for completed_date** — If you use timestamptz, a user completing a habit at 11 PM and again at 1 AM on the next day would create two rows that look like the same calendar day in some time zones but different days in others. Fix: Use the Postgres date type for completed_date and always pass the local date string from the client (new Date().toISOString().split('T')[0]). All date comparisons in the streak function work with calendar dates, not timestamps.
- **Fetching streaks for all habits simultaneously on dashboard load** — If a user has 10 habits, calling calculate_streaks 10 times in parallel is 10 database round trips on every page load. This becomes slow and expensive. Fix: Create a single get_all_streaks(p_user_id uuid) Postgres function that returns streaks for all of the user's habits in one query. Ask Lovable to write it using a GROUP BY approach on the completions table.
- **Not handling the case where today is not yet completed when calculating current streak** — If a user checks the app in the morning before completing any habits, a strict consecutive-day calculation would show streak = 0 because today has no completion. This breaks long streaks every morning. Fix: The streak function should treat today as optional: start counting from yesterday if today has no completion yet. The streak should only break if yesterday AND today both have no completion.
- **Rendering the heatmap without a loading skeleton** — The heatmap query fetches 84 days of data. On a slow connection, the component renders empty and then suddenly fills in, causing layout shift. Fix: Show a Skeleton component in the shape of the heatmap grid while the data loads. Use the shadcn/ui Skeleton component. This is straightforward: render 84 skeleton squares in the same grid layout.

## Best practices

- Use the date type (not timestamptz) for completed_date and always derive it from the user's local time zone using toLocaleDateString or toISOString().split('T')[0].
- Add the unique constraint on (habit_id, completed_date) at the database level. Application-level dedup is not sufficient — concurrent requests could both pass the check before either inserts.
- Implement optimistic updates for the completion toggle. The primary user interaction in a habit tracker is toggling habits done. Any perceptible delay here damages the feel of the app significantly.
- Do not delete habit_completions rows when archiving a habit. Mark the habit as is_archived = true and filter archived habits from the dashboard. The completion history is preserved for stats.
- Index habit_completions on (user_id, completed_date DESC) to make the heatmap query fast. Index on (habit_id, completed_date DESC) for per-habit streak calculations.
- Show encouraging language when streaks are maintained (not just numbers). '7-day streak — keep going!' is more motivating than '7'. Add a subtle animation when a habit is completed.

## Frequently asked questions

### What happens to my streak if I forget to log a habit?

The streak breaks when there is a gap of more than one day in completions. You can add a grace period feature: if yesterday has no completion but the day before does, still count it. Store this preference per user. Most users prefer strict streaks because they are more motivating, but a grace period reduces the frustration of occasional misses.

### Can I track habits I want to do multiple times per day?

The default schema supports only one completion per habit per day (enforced by the unique constraint). To track multiple completions per day, remove the unique constraint and add a target_count column to habits. The toggle becomes an incrementor. The streak function counts a day as completed when the completion count for that day reaches target_count.

### How do I add a habit that is only for weekdays?

Set the frequency to 'weekdays' in the habits table. In the dashboard, only show the completion toggle for habits on their relevant days. In the streak calculation, skip days that are not part of the habit's schedule when checking for gaps. A 'weekday only' habit does not break its streak over the weekend.

### Will free Supabase tier handle a habit tracker?

Yes comfortably. A single user completing 10 habits daily for 1 year generates 3,650 rows in habit_completions. Even with 100 active users, that is under 500,000 rows — well within the Supabase free tier's 500MB limit. The free tier supports up to 2 projects and 50,000 monthly active users.

### How does the heatmap handle different time zones?

The heatmap uses completed_date which is a calendar date, not a timestamp. Always derive the date string on the client using the user's local time: new Date().toLocaleDateString('en-CA') returns YYYY-MM-DD in local time. This ensures that completing a habit at 11 PM in Tokyo counts for the correct local date.

### Can I export my habit history as data?

Add an Export button on the Stats page that queries all habit_completions for the user, formats them as CSV (date, habit name, completed), and triggers a browser download. This is a simple client-side operation: build the CSV string in JavaScript from the fetched array and create a Blob URL.

### How do I handle habits I want to track but not every day?

Use the custom_days array in habits (0=Sunday to 6=Saturday). Store which days the habit is active. The dashboard only shows the toggle for active days. The streak function skips non-scheduled days when checking for gaps. For example, a habit scheduled only on Tuesday and Thursday will not break its streak on other days.

### Is there help building a habit tracker with team features or coaching tools?

RapidDev builds Lovable apps with coach-client relationships, shared accountability groups, and custom notification systems. Reach out if you need features beyond individual habit tracking.

---

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