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
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
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.
1Create a habit tracking schema in Supabase.23Tables: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_at5- habit_completions: id, habit_id (references habits), user_id (references auth.users), completed_date (date), notes (text, nullable), created_at67Unique constraint on habit_completions(habit_id, completed_date) to prevent duplicate completions for the same day.89Create 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 DESC11- 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.1415RLS:16- habits: users SELECT/INSERT/UPDATE/DELETE their own (user_id = auth.uid())17- habit_completions: users SELECT/INSERT/DELETE their own rows1819Create 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.
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.
1Build a habits dashboard at src/pages/Dashboard.tsx.23Requirements:4- Fetch all non-archived habits for the current user5- For each habit, check if a habit_completions row exists for today's date6- 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 card9 - 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 checked11 - A menu (three-dot icon) with options: Edit, Archive, View Stats12- 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 percentage14- 'Add Habit' Button opening a Sheet with the habit creation formExpected 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.
Add the calendar heatmap
Ask Lovable to build the GitHub-style contribution heatmap showing 12 weeks of habit completion history.
1Add a calendar heatmap component at src/components/HabitHeatmap.tsx.23Requirements:4- Accept props: completionsByDate (Record<string, number>) where key is 'YYYY-MM-DD' and value is count of habits completed that day5- 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-6007- Each square is a Tooltip-wrapped div showing the date and completion count on hover8- Show month labels above the grid where the month changes9- Show day labels (M, W, F) on the left side10- 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_date11- Render this component on the Dashboard page below the habit listExpected 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.
Build the stats page with charts
A dedicated stats page gives users deeper insight into their consistency. Ask Lovable to build it with Recharts.
1Build a stats page at src/pages/Stats.tsx.23Requirements: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 data9- 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 color12 - The heatmap component from Step 313- All charts use Recharts with custom tooltips showing formatted date and value14- Add a month/year selector to change the time period for chartsExpected 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
1import { useState } from 'react'2import { supabase } from '@/integrations/supabase/client'3import { useToast } from '@/components/ui/use-toast'45type HabitCompletion = {6 id: string7 habit_id: string8 completed_date: string9}1011export function useHabitToggle(12 habitId: string,13 initialCompletion: HabitCompletion | null14) {15 const [completion, setCompletion] = useState<HabitCompletion | null>(initialCompletion)16 const [isLoading, setIsLoading] = useState(false)17 const { toast } = useToast()1819 const today = new Date().toISOString().split('T')[0]2021 const toggle = async () => {22 if (isLoading) return23 setIsLoading(true)2425 const previousCompletion = completion2627 if (completion) {28 // Optimistic: mark incomplete29 setCompletion(null)30 const { error } = await supabase31 .from('habit_completions')32 .delete()33 .eq('id', completion.id)3435 if (error) {36 setCompletion(previousCompletion)37 toast({ title: 'Could not update habit', variant: 'destructive' })38 }39 } else {40 // Optimistic: mark complete with a temporary id41 const tempCompletion = { id: 'temp', habit_id: habitId, completed_date: today }42 setCompletion(tempCompletion)4344 const { data, error } = await supabase45 .from('habit_completions')46 .insert({ habit_id: habitId, completed_date: today })47 .select()48 .single()4950 if (error) {51 setCompletion(null)52 toast({ title: 'Could not update habit', variant: 'destructive' })53 } else if (data) {54 setCompletion(data)55 }56 }5758 setIsLoading(false)59 }6061 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.
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.
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.
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.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation