Skip to main content
RapidDev - Software Development Agency

How to Build a Habit Tracker with Lovable

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.

What you'll build

  • Habit creation form with name, color, icon, and target frequency (daily or specific days)
  • Daily completion toggle that marks habits done for today
  • Streak calculation via a Postgres function returning current and longest streaks
  • Calendar heatmap showing the last 12 weeks of completion data
  • Weekly bar chart showing completion rate per habit for the past 7 days
  • Monthly summary showing total completions, best streak, and perfect days
  • Habit archive to remove inactive habits without losing history
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner12 min read1–1.5 hoursLovable free tier or higherApril 2026RapidDev Engineering Team
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.

What you're building

Habit tracking has a deceptively simple data model: a habits table (what you track) and a habit_completions table (when you completed it). The streak calculation is where the interesting logic lives.

A streak is consecutive days with at least one completion. The Postgres function calculate_streaks(p_user_id, p_habit_id) uses a recursive CTE to walk backwards through completion dates, counting consecutive days until it hits a gap. It returns both the current streak (from today backwards) and the all-time longest streak.

The calendar heatmap queries completions grouped by date for the last 84 days (12 weeks). Each day square is colored by the count of habits completed that day: 0=empty, 1-2=light, 3-4=medium, 5+=dark. This gives users a visual sense of their overall consistency without focusing on any single habit.

The completion toggle is optimistic — it updates the local state immediately and syncs to Supabase in the background. If the sync fails, it reverts. This makes the app feel instant even on slow connections.

Final result

A focused habit tracker where streaks are automatic, the heatmap makes consistency visible, and checking off habits feels satisfying.

Tech stack

LovableFrontend app
SupabaseDatabase with RLS
shadcn/uiUI components
RechartsWeekly bar chart and monthly trends
Supabase AuthPer-user habit isolation

Prerequisites

  • 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

Build steps

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.

prompt.txt
1Create a habit tracking schema in Supabase.
2
3Tables:
4- 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
5- habit_completions: id, habit_id (references habits), user_id (references auth.users), completed_date (date), notes (text, nullable), created_at
6
7Unique constraint on habit_completions(habit_id, completed_date) to prevent duplicate completions for the same day.
8
9Create a Postgres function calculate_streaks(p_habit_id uuid, p_user_id uuid) that returns (current_streak int, longest_streak int):
10- Get all completed_date values for this habit, ordered DESC
11- 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.
12- Longest streak: find the maximum run of consecutive dates in the full history.
13- Return both values.
14
15RLS:
16- habits: users SELECT/INSERT/UPDATE/DELETE their own (user_id = auth.uid())
17- habit_completions: users SELECT/INSERT/DELETE their own rows
18
19Create 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.

prompt.txt
1Build a habits dashboard at src/pages/Dashboard.tsx.
2
3Requirements:
4- Fetch all non-archived habits for the current user
5- For each habit, check if a habit_completions row exists for today's date
6- Display habits as a list of Cards. Each Card shows:
7 - A large Checkbox (or Toggle) on the left. Clicking it creates or deletes a habit_completions row for today.
8 - Habit name, description (truncated), and color indicator stripe on the left edge of the card
9 - Current streak displayed as a flame icon + number (e.g. '🔥 7 days'). Fetch this from calculate_streaks via supabase.rpc().
10 - Completed cards have a green background tint and the checkbox is checked
11 - A menu (three-dot icon) with options: Edit, Archive, View Stats
12- Use optimistic updates: clicking the toggle updates local state immediately. If the Supabase call fails, revert and show a Toast error.
13- Progress bar at the top: 'X of Y habits done today' with percentage
14- '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.

prompt.txt
1Add a calendar heatmap component at src/components/HabitHeatmap.tsx.
2
3Requirements:
4- Accept props: completionsByDate (Record<string, number>) where key is 'YYYY-MM-DD' and value is count of habits completed that day
5- Render a 12-week grid (84 day squares) from 83 days ago to today, arranged as 7 rows (Mon-Sun) x 12 columns (weeks)
6- 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
7- Each square is a Tooltip-wrapped div showing the date and completion count on hover
8- Show month labels above the grid where the month changes
9- Show day labels (M, W, F) on the left side
10- 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
11- 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.

prompt.txt
1Build a stats page at src/pages/Stats.tsx.
2
3Requirements:
4- Habit selector at the top: a Select component listing all habits. Default = 'All habits'.
5- When a specific habit is selected, show:
6 - Stat Cards: Current Streak, Longest Streak, Total Completions, Completion Rate (completions / days since habit was created)
7 - LineChart: completion rate per week (percentage, last 12 weeks)
8 - Call calculate_streaks via supabase.rpc() for the streak data
9- When 'All habits' is selected, show:
10 - Stat Cards: Total habits, Habits completed today, Perfect days (days where all active habits were completed), Best day (date with most completions)
11 - BarChart: per-habit completion count for the last 30 days, one bar per habit, colored with the habit's color
12 - The heatmap component from Step 3
13- All charts use Recharts with custom tooltips showing formatted date and value
14- 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

src/hooks/useHabitToggle.ts
1import { useState } from 'react'
2import { supabase } from '@/integrations/supabase/client'
3import { useToast } from '@/components/ui/use-toast'
4
5type HabitCompletion = {
6 id: string
7 habit_id: string
8 completed_date: string
9}
10
11export function useHabitToggle(
12 habitId: string,
13 initialCompletion: HabitCompletion | null
14) {
15 const [completion, setCompletion] = useState<HabitCompletion | null>(initialCompletion)
16 const [isLoading, setIsLoading] = useState(false)
17 const { toast } = useToast()
18
19 const today = new Date().toISOString().split('T')[0]
20
21 const toggle = async () => {
22 if (isLoading) return
23 setIsLoading(true)
24
25 const previousCompletion = completion
26
27 if (completion) {
28 // Optimistic: mark incomplete
29 setCompletion(null)
30 const { error } = await supabase
31 .from('habit_completions')
32 .delete()
33 .eq('id', completion.id)
34
35 if (error) {
36 setCompletion(previousCompletion)
37 toast({ title: 'Could not update habit', variant: 'destructive' })
38 }
39 } else {
40 // Optimistic: mark complete with a temporary id
41 const tempCompletion = { id: 'temp', habit_id: habitId, completed_date: today }
42 setCompletion(tempCompletion)
43
44 const { data, error } = await supabase
45 .from('habit_completions')
46 .insert({ habit_id: habitId, completed_date: today })
47 .select()
48 .single()
49
50 if (error) {
51 setCompletion(null)
52 toast({ title: 'Could not update habit', variant: 'destructive' })
53 } else if (data) {
54 setCompletion(data)
55 }
56 }
57
58 setIsLoading(false)
59 }
60
61 return { isCompleted: !!completion, toggle, isLoading }
62}

Customization ideas

Habit reminders via browser notifications

Add a reminder_time column to habits (type time). Use the Web Notifications API to schedule a notification at the specified time. Request notification permission during onboarding. Store the last notification sent date so reminders only fire once per day.

Habit grouping and categories

Add a category column to habits (Morning Routine, Health, Work, Personal). Group the dashboard view by category with collapsible sections. Show a per-category completion progress bar. Users can reorder habits within categories via drag-and-drop.

Habit buddy — shared accountability

Add a habit_shares table where users can share a read-only view of specific habits with a friend. The friend gets a public URL showing the habit's streak and heatmap without any editing ability. Add a Share button to each habit that generates a unique share link stored in Supabase.

Weekly review digest

Every Sunday, trigger an Edge Function via a Supabase scheduled cron job that calculates the week's statistics for each user: habits completed, streaks maintained, and any new milestones. Send a summary email via Resend with a motivational message and a link to the stats page.

Milestone celebrations

Define streak milestones (7, 21, 30, 60, 100 days). After each habit completion, check if the current streak just crossed a milestone threshold. If so, trigger a confetti animation and insert a milestone_achievements row. Show a history of milestones on the stats page.

Common pitfalls

Pitfall: Using a timestamp instead of a date type for completed_date

How to avoid: 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.

Pitfall: Fetching streaks for all habits simultaneously on dashboard load

How to avoid: 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.

Pitfall: Not handling the case where today is not yet completed when calculating current streak

How to avoid: 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.

Pitfall: Rendering the heatmap without a loading skeleton

How to avoid: 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.

AI prompts to try

Copy these prompts to build this project faster.

ChatGPT Prompt

I'm building a habit tracker in Supabase. I have a habit_completions table with columns habit_id (uuid) and completed_date (date). I need a PostgreSQL function calculate_streaks(p_habit_id uuid, p_user_id uuid) that returns the current streak and longest streak. The current streak should NOT break if today has no completion yet — it should start counting from yesterday. Show me the full SQL function using window functions or a recursive CTE approach.

Lovable Prompt

Add a 'Monthly Review' page at /review. For the current month, show: a calendar view with each day marked as Perfect Day (all habits completed = green background), Partial Day (some completed = yellow), or Missed Day (none = gray). Below the calendar, a summary: total perfect days, habits with 100% completion rate this month (Badge for each), habits that need improvement (completion rate under 50%, shown with an encouraging suggestion). Add previous/next month navigation buttons.

Build Prompt

In Supabase, write a SQL function get_heatmap_data(p_user_id uuid, p_days int) that returns rows of (completed_date date, habit_count int) for the last p_days days. Include dates with zero completions by generating a date series using generate_series(current_date - p_days, current_date, '1 day'::interval) and LEFT JOINing habit_completions on completed_date. This ensures missing dates show up as 0 in the heatmap rather than being absent from the result.

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.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help building your app?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.