Feature spec
IntermediateCategory
analytics-admin
Build with AI
4-8 hours with Lovable or FlutterFlow
Custom build
1-2 weeks custom dev
Running cost
$0/mo up to ~1,000 users; $25-50/mo at 10,000 users
Works on
Everything it takes to ship User Progress Tracking — parts, prompts, and real costs.
User progress tracking needs four pieces: a progress bar component, a table recording completed steps per user, a calculation function that returns 0-100%, and streak/badge logic for retention. With Lovable or FlutterFlow you can ship a working system in 4-8 hours for $0/month on Supabase's free tier — costs only rise past 1,000 users when realtime connection limits are hit.
What User Progress Tracking Actually Is
Progress tracking shows users how far along they are in a course, onboarding flow, or workout program — with a visual bar or ring that updates the moment they complete each step. The design decision behind it is retention: users who see 60% complete are far more likely to return than users staring at an unstructured list. The feature has three layers: the visual component (bar, ring, step dots), the data layer recording which steps each user finished, and optional engagement hooks like streaks and milestone badges. The visual part is the easy half; making progress persist across devices and sessions is where most first builds break.
What users consider table stakes in 2026
- Visual progress bar or ring that updates instantly when a step is completed — no page reload required
- Percentage shown numerically alongside the visual (e.g. '4 of 7 steps · 57%')
- Milestone badges or checkmarks appear when the user hits 25%, 50%, and 100%
- Resume exactly where the user left off — the last visited step is highlighted on return
- Summary screen listing completed vs remaining items so users can plan their next session
- Streak counter showing current streak and last-active date to encourage daily return visits
Anatomy of the Feature
Six components across the UI, data, and backend layers. Lovable and FlutterFlow handle the visual and data layers well on a first prompt; the streak cron and milestone badge engine reliably need a second pass.
Progress bar / ring component
UIRenders completion percentage as a filled bar or circular ring. On web: shadcn/ui Progress component (built on Radix UI) with an animated fill driven by a 0-100 value. On mobile: Flutter's LinearProgressIndicator or CircularProgressIndicator with an AnimatedContainer wrapping it for smooth transitions.
Note: For step-dot progress (e.g. onboarding checklist), render each step as a circle that fills on completion — more scannable than a single bar for 5-10 steps.
Steps or modules table
DataPostgreSQL table tracking which steps each user has completed: user_id, course_id, step_id, status (started/completed), and completed_at timestamp. Queried via Supabase JS client (.from('user_progress').select()) or FlutterFlow's Supabase Backend Query action.
Note: Include a step_order INT column on the course_steps table so you can enforce sequential progression if needed — AI tools generate unordered steps by default.
Progress calculation function
BackendA Supabase Edge Function (calculate_progress) or Next.js Server Action that counts completed steps / total steps per user per course and returns a 0-100 float. Guards against division by zero when total_steps is NULL or 0.
Note: For web, this can run as a Supabase computed column (pure SQL) rather than an Edge Function — simpler and cheaper for straightforward percentage calculations.
Milestone / badge engine
BackendEvaluates completion thresholds (25%, 50%, 100%) after each step is marked complete and inserts rows into user_achievements when a threshold is crossed. Emits a Supabase Realtime event on the user_achievements channel so the frontend can show a badge pop-up without polling.
Note: Use ON CONFLICT DO NOTHING on the user_achievements insert so re-triggering the function cannot award the same badge twice.
Streak tracker
DataRecords daily activity in a user_streaks table (user_id, current_streak INT, longest_streak INT, last_active_date DATE). A pg_cron job running daily at 00:01 UTC sets current_streak = 0 for users whose last_active_date is before yesterday, preserving longest_streak if current was higher.
Note: If pg_cron is not available on your Supabase tier, an n8n Cloud schedule (€20/mo approx) calling a Supabase Edge Function achieves the same reset without database extensions.
RLS policy layer
BackendRow-Level Security on user_progress, user_achievements, and user_streaks ensures every SELECT, INSERT, and UPDATE is scoped to auth.uid() = user_id. Configured in Supabase Dashboard → Authentication → Policies.
Note: Without RLS, any authenticated user can read or overwrite another user's progress — AI tools generate the schema but frequently forget the INSERT policy's WITH CHECK clause.
The data model
Four tables covering courses, per-step progress, badges, and streaks. Paste this into the Supabase SQL Editor (Dashboard → SQL Editor → New Query) and run it before prompting your AI tool:
1create table public.courses (2 id uuid primary key default gen_random_uuid(),3 title text not null,4 total_steps int not null default 15);67create table public.user_progress (8 id uuid primary key default gen_random_uuid(),9 user_id uuid references auth.users(id) on delete cascade not null,10 course_id uuid references public.courses(id) on delete cascade not null,11 step_id text not null,12 status text not null default 'completed' check (status in ('started', 'completed')),13 completed_at timestamptz,14 created_at timestamptz not null default now(),15 unique (user_id, course_id, step_id)16);1718create table public.user_achievements (19 id uuid primary key default gen_random_uuid(),20 user_id uuid references auth.users(id) on delete cascade not null,21 badge_slug text not null,22 earned_at timestamptz not null default now(),23 unique (user_id, badge_slug)24);2526create table public.user_streaks (27 user_id uuid primary key references auth.users(id) on delete cascade,28 current_streak int not null default 0,29 longest_streak int not null default 0,30 last_active_date date31);3233-- RLS34alter table public.user_progress enable row level security;35alter table public.user_achievements enable row level security;36alter table public.user_streaks enable row level security;3738create policy "Users read own progress"39 on public.user_progress for select40 using (auth.uid() = user_id);4142create policy "Users insert own progress"43 on public.user_progress for insert44 with check (auth.uid() = user_id);4546create policy "Users update own progress"47 on public.user_progress for update48 using (auth.uid() = user_id);4950create policy "Users read own achievements"51 on public.user_achievements for select52 using (auth.uid() = user_id);5354create policy "Users read own streaks"55 on public.user_streaks for select56 using (auth.uid() = user_id);5758create policy "Users update own streaks"59 on public.user_streaks for update60 using (auth.uid() = user_id);6162create policy "Users insert own streaks"63 on public.user_streaks for insert64 with check (auth.uid() = user_id);6566-- Indexes67create index user_progress_lookup_idx68 on public.user_progress (user_id, course_id);6970create index user_progress_step_idx71 on public.user_progress (course_id, step_id);Heads up: The unique constraint on (user_id, course_id, step_id) lets you use upsert with onConflict instead of insert — progress cannot be double-counted even if the same step is submitted twice.
Build it — pick your path
Each tool fits this feature differently. Switch tabs to compare fit, time, the exact steps, and a copy-paste prompt.
Best path for mobile-native feel: Flutter's LinearProgressIndicator and CircularProgressIndicator animate smoothly, and FlutterFlow's Supabase Backend Query actions handle real-time updates via Realtime Stream with minimal custom code.
Step by step
- 1Create a FlutterFlow project connected to your Supabase project and import the schema tables
- 2Design the course detail page: add a LinearProgressIndicator widget bound to a page state variable (completionPct); add a ListView of step items with Checkbox widgets
- 3Wire each Checkbox to a Supabase upsert action (Action editor → Supabase Row → Upsert) writing to user_progress with the conflict resolution on user_id+course_id+step_id
- 4After the upsert action, add a Backend Query that counts completed steps / total_steps and updates the completionPct page state variable — the progress bar animates automatically
- 5Switch the course query to a Realtime Stream action (Backend Query → Set as Realtime Stream) so the bar updates if another device completes a step for the same user
- 6In FlutterFlow Settings → Permissions, enable camera and photo library access descriptions if you plan to add avatars to the streak screen
Where this path bites
- Code export requires FlutterFlow Pro ($70/mo) — the free plan runs in the FlutterFlow mobile preview app only
- Custom animations for the badge pop-up (confetti) require a Dart custom code block outside the visual builder
- Firestore is easier to connect than Supabase for first-time FlutterFlow users; follow the Supabase FlutterFlow docs carefully for the anon key setup
Third-party services you'll need
Progress tracking itself runs entirely on Supabase with no third-party services required. You only add external services for streak reset scheduling if pg_cron is unavailable:
| Service | What it does | Free tier | Paid from |
|---|---|---|---|
| Supabase | Progress data storage, RLS, Realtime events for live badge pop-ups, pg_cron for daily streak reset | Free tier: 500MB DB, 2 projects, 50K Realtime messages/mo | Pro $25/mo (8GB DB, higher Realtime connection limits) |
| n8n (optional) | Schedule-based streak reset workflow if pg_cron is not enabled — calls a Supabase Edge Function at midnight UTC daily | Self-hosted free (requires server) | Cloud Starter €20/mo (approx) for 2,500 executions |
Swipe the table sideways to see pricing.
What it costs to run
Drag through the tiers to see how your monthly bill scales with users — no surprises later.
Estimated monthly running cost
Supabase free tier covers the storage and reads easily — 100 active users completing steps generates well under 500MB of data. pg_cron streak reset runs within free tier limits.
Estimates use verified 2026 pricing and assume typical usage per user. Your real bill depends on activity, storage, and third-party plans.
What breaks when AI tools build this
The failures people actually hit on their first build — the symptom, why it happens, and the exact fix.
Progress resets to 0% on page reload
Symptom: The AI generates optimistic local state that increments the bar on click but is never actually persisted to Supabase — the INSERT into user_progress is either missing or fires without awaiting the result, so the database row is never created. On reload, the calculated percentage is 0 because no completed rows exist.
Fix: Add an explicit upsert with onConflict: 'user_id,course_id,step_id' in the Supabase client call and verify the row appears in the Table Editor (Supabase Dashboard → Table Editor → user_progress) before testing the UI behaviour.
Streak counter never resets even after missed days
Symptom: The pg_cron job is described in the prompt and the AI writes the SQL function correctly, but Supabase pg_cron is not enabled by default on any tier — the extension must be turned on manually. Without it, current_streak just keeps accumulating regardless of activity.
Fix: Enable pg_cron via Supabase Dashboard → Database → Extensions, search for 'pg_cron', and toggle it on. Then test the reset function by manually running it from the SQL Editor: SELECT cron.schedule('streak-reset', '1 0 * * *', 'SELECT reset_inactive_streaks()').
Progress bar shows NaN% for new courses
Symptom: The percentage calculation divides the completed step count by total_steps, but total_steps is NULL for newly created courses that haven't had the column value set. JavaScript's division returns NaN, which displays as an empty or broken bar.
Fix: Add a NOT NULL DEFAULT 1 constraint on courses.total_steps (included in the SQL schema above) and guard the calculation: Math.round((completed / (total || 1)) * 100). The || 1 fallback prevents division by zero without hiding the real bug.
RLS silently blocks step completion writes
Symptom: Supabase RLS is enabled on user_progress but the INSERT policy generated by the AI is missing the WITH CHECK clause or uses the wrong condition. Anonymous or wrong-user writes fail silently — the Supabase client returns no error message, the row is never created, and progress appears to save but resets on reload.
Fix: Open Dashboard → Authentication → Policies → user_progress and confirm the INSERT policy reads: WITH CHECK (auth.uid() = user_id). If the policy is missing the WITH CHECK, the USING clause alone does not protect inserts.
Mobile: progress bar does not update until pull-to-refresh
Symptom: In FlutterFlow, the Backend Query that fetches completion data is set to 'Query once' rather than subscribing to a Realtime Stream. When the user completes a step on another device — or when the upsert action fires faster than the UI re-renders — the progress bar shows stale data until the user manually refreshes.
Fix: In FlutterFlow's Backend Query settings for the course detail page, switch the query mode to 'Realtime Stream' and filter by user_id = currentUserUid and course_id = the current course ID. The LinearProgressIndicator will animate to the new value automatically on each Realtime event.
Best practices
Use upsert with an explicit onConflict key (user_id, course_id, step_id) instead of insert — it prevents duplicate rows even when the user clicks a step checkbox rapidly or across two browser tabs
Guard every percentage calculation against NaN: Math.round((completed / (total || 1)) * 100) — a missing total_steps value should never break the UI
Show the last incomplete step as 'Resume here' on the course landing page — this single change measurably improves return-visit completion rates
Store last_active_date in UTC and document that streak resets happen at midnight UTC — users in UTC-8 will lose their streak at 4 PM local time, so set expectations in the UI
Award badges with ON CONFLICT DO NOTHING so re-running the badge evaluation function (e.g. on a page refresh) cannot double-award the same milestone
Subscribe to Supabase Realtime on the user_achievements channel to show badge pop-ups without polling — polling every 5 seconds on 10,000 users costs real read quota
Give admins a read-only view of every user's completion percentage — without it, you will not know when users get stuck at the same step and need content changes
When You Need Custom Development
Lovable and FlutterFlow handle linear progress tracking — step A, then B, then C — very well. You outgrow them when the rules around progression get complex:
- Curriculum has branching or prerequisite logic where step B only unlocks after both step A and step C are complete — this requires a dependency graph resolver that AI tools cannot generate correctly
- Progress must sync bidirectionally with an external LMS via xAPI or SCORM 1.2 — the specification alone takes a developer a week to implement correctly
- Cohort-based tracking where admins can override, reset, or grant progress on behalf of individual users requires a service-role audit trail beyond what RLS allows
- Compliance requirements mandate an immutable, timestamped audit log of every completion event — an append-only event sourcing pattern that AI tools do not produce
RapidDev builds these features for production
Real auth, database, and payments — fixed price, delivered in weeks. You own 100% of the code.
Frequently asked questions
How do I save progress automatically without a Save button?
Wire each step checkbox or completion event directly to a Supabase upsert call. The upsert writes the row immediately on click — no Save button needed. Use upsert with onConflict: 'user_id,course_id,step_id' so rapid double-clicks don't create duplicate rows.
Can I show progress across multiple courses on a single dashboard?
Yes — query user_progress grouped by course_id and join to courses.total_steps to calculate a percentage per course. Render one Progress bar per course in a grid. The SQL in the schema above and the V0 admin path show exactly this pattern.
How do I prevent users from skipping required steps?
Add a prerequisite check in the step completion handler: before inserting into user_progress for step N, query whether step N-1 (or all prerequisites) are already completed. If not, return an error and show a tooltip explaining which step must come first. This logic needs to be explicit in your prompt — AI tools generate free-order completion by default.
How do I award badges when milestones are reached?
After every step completion, calculate the new percentage server-side. If it crosses 25%, 50%, or 100%, upsert a row into user_achievements with the corresponding badge_slug and ON CONFLICT DO NOTHING to prevent duplicates. Subscribe to the user_achievements Realtime channel on the client to show a badge pop-up without polling.
Will progress persist if a user switches devices?
Yes — because progress is stored in Supabase (server-side), not in localStorage. Any device that authenticates with the same user account will read the same completed steps and show the correct percentage. This is why the upsert must actually write to the database and not just update local React state.
How do I let admins view or reset individual user progress?
Create an admin route (guarded by a user_roles check) that queries user_progress grouped by user and course. To reset a user's progress, delete their rows from user_progress and user_achievements for the target course — add a confirmation modal so resets are not accidental. Requires a service-role Supabase client in the API route to bypass RLS.
How do I add a streak system that emails users when they are about to break it?
The streak reset pg_cron job runs at 00:01 UTC. Add a second job at 20:00 UTC that queries users where last_active_date = yesterday (streak is still alive but 4 hours from resetting) and sends each an email via Resend or your transactional email provider. Pass the user's current_streak count in the email subject for personalization.
Need this feature production-ready?
RapidDev builds user progress tracking into real apps — auth, database, payments — at $13K–$25K.
Book a free consultation30-min call. No commitment.