Skip to main content
RapidDev - Software Development Agency
App Featuresanalytics-admin21 min read

How to Add a Content Challenge Tracker to Your App — Copy-Paste Prompts Inside

A content challenge tracker needs a check-in mechanism, a streak engine in Postgres, push reminders via Firebase Cloud Messaging, and a leaderboard view. With FlutterFlow and Supabase you can ship a fully working system in 1–2 days for $0/month up to ~1,000 users. The hard parts — timezone-safe streak calculation and FCM token refresh — are handled server-side in a Supabase Edge Function plus a Postgres function.

4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members

Feature spec

Intermediate

Category

analytics-admin

Build with AI

1-2 days with FlutterFlow + Supabase

Custom build

1-2 weeks custom dev

Running cost

$0/mo up to ~1K users (Supabase free + FCM free tier)

Works on

Mobile

Everything it takes to ship a Content Challenge Tracker — parts, prompts, and real costs.

TL;DR

A content challenge tracker needs a check-in mechanism, a streak engine in Postgres, push reminders via Firebase Cloud Messaging, and a leaderboard view. With FlutterFlow and Supabase you can ship a fully working system in 1–2 days for $0/month up to ~1,000 users. The hard parts — timezone-safe streak calculation and FCM token refresh — are handled server-side in a Supabase Edge Function plus a Postgres function.

What a Content Challenge Tracker Actually Is

A content challenge tracker turns a passive habit into a social game: users commit to posting daily or weekly, a streak counter rewards consistency, and a leaderboard shows how they rank against peers. The mechanics — check-in button, streak calculation, push reminder before deadline, shareable badge on completion — are a proven retention loop used by Duolingo, Strava, and every fitness app with a following. The implementation has three distinct layers: a mobile UI built in FlutterFlow for the calendar strip, progress rings, and leaderboard; a Postgres function (streak_calculator) that counts consecutive days accurately across timezones; and a Supabase Edge Function that sends FCM push notifications 2 hours before the daily deadline. Skipping any layer produces a tracker that looks right but counts streaks wrong or goes silent when users expect a nudge.

What users consider table stakes in 2026

  • Streak counter with a flame icon that resets visibly on the first missed day — not silently the next morning
  • Daily or weekly challenge calendar strip with per-day status dots (pending, complete, missed) and a check-in button for today
  • Leaderboard showing at least the top 20 participants ranked by current streak, refreshable with pull-to-refresh
  • Push notification reminder arriving at a fixed time (e.g. 9pm local) before the daily deadline, with per-challenge opt-in/opt-out
  • Personal progress dashboard showing completion rate, longest streak, and total check-ins with a circular progress ring
  • Shareable challenge badge or certificate that generates on completion and can be exported or shared via the native share sheet

Anatomy of the Feature

Six components. FlutterFlow builds the two UI components correctly on a single prompt. The streak engine and push scheduler are the parts that require direct work in Supabase — AI tools cannot configure pg_cron or FCM for you.

Layers:UIDataBackendService

Challenge calendar strip

UI

Horizontal scrollable date strip built with Flutter's ListView.builder and DateUtils. Each day cell shows a status icon — pending ring, green checkmark for complete, grey X for missed. Today's cell is highlighted. Tapping an incomplete today-cell opens the check-in modal; past cells are read-only.

Note: Use DateUtils.isSameDay() for cell comparison — never compare raw DateTime milliseconds, which breaks across DST boundaries.

Check-in action layer

Backend

Records a timestamped entry in the Supabase challenge_entries table. The insert is idempotent thanks to a UNIQUE(challenge_id, user_id, entry_date) constraint — double-taps and retry logic cannot inflate the count. After a successful insert, Supabase Realtime broadcasts the new count to any open admin or leaderboard views.

Note: Store entry_date as a DATE in the user's local timezone (not UTC). Pass the client's timezone offset at check-in time and convert inside the Postgres function — see Gotcha 1.

Streak engine

Backend

A Postgres function named streak_calculator fires as an AFTER INSERT trigger on challenge_entries. It counts consecutive calendar days with entries going backward from today, writes current_streak and longest_streak to the user_streaks table, and sets last_entry_date. If the streak breaks, it triggers the Edge Function to send a broken-streak notification via FCM.

Note: Write and test this function directly in the Supabase SQL editor — FlutterFlow has no visual interface for Postgres triggers.

Progress dashboard

UI

A Flutter page using the fl_chart package (free, pub.dev) to render a circular progress ring showing completion percentage. Below the ring: three stat chips — longest streak, current streak, total check-ins — populated from user_streaks and a COUNT query on challenge_entries via Supabase .select().

Note: fl_chart's RadialBarChart handles the ring. Pull values once on page load and cache in FlutterFlow page state to avoid re-querying on every rebuild.

Leaderboard

Data

A Supabase database view (v_leaderboard) ordered by current_streak DESC, joining user_streaks with auth.users to expose a display name. The FlutterFlow page queries this view, renders rows in a ListView with rank number badges (1st = gold, 2nd = silver, 3rd = bronze), and supports pull-to-refresh and pagination limited to top 100.

Note: The default RLS policy on user_streaks blocks reads of other users' rows — add a SELECT-only policy with USING (true) on user_streaks so the leaderboard view can assemble all rows. See Gotcha 5.

Push notification scheduler

Service

A Supabase Edge Function (Deno) triggered by pg_cron on a schedule (e.g. daily at 19:00 UTC). It queries challenge_entries to find users who have NOT checked in yet today, looks up their FCM device tokens from user_devices, and calls the FCM v1 API to send a reminder push. Device tokens are refreshed on every app launch and stored via upsert.

Note: FCM v1 API requires OAuth 2.0 authentication with a service account key — store the JSON key as a Supabase Secret, not in code. The legacy FCM API was deprecated in June 2024.

The data model

Five tables cover the full feature. Run this in the Supabase SQL editor (Dashboard → SQL Editor → New query). The UNIQUE constraint on challenge_entries is what prevents double check-ins — do not omit it.

schema.sql
1-- Challenges (admin-created or user-created depending on your config)
2create table public.challenges (
3 id uuid primary key default gen_random_uuid(),
4 title text not null,
5 frequency text not null check (frequency in ('daily', 'weekly')),
6 start_date date not null,
7 end_date date not null,
8 created_by uuid references auth.users(id) on delete set null,
9 max_participants int,
10 created_at timestamptz default now()
11);
12
13-- One row per user per day per challenge
14create table public.challenge_entries (
15 id uuid primary key default gen_random_uuid(),
16 challenge_id uuid references public.challenges(id) on delete cascade not null,
17 user_id uuid references auth.users(id) on delete cascade not null,
18 entry_date date not null,
19 created_at timestamptz default now(),
20 constraint uq_challenge_user_date unique (challenge_id, user_id, entry_date)
21);
22
23-- Streak tallies updated by streak_calculator trigger
24create table public.user_streaks (
25 challenge_id uuid references public.challenges(id) on delete cascade not null,
26 user_id uuid references auth.users(id) on delete cascade not null,
27 current_streak int not null default 0,
28 longest_streak int not null default 0,
29 last_entry_date date,
30 primary key (challenge_id, user_id)
31);
32
33-- FCM device tokens, refreshed on every app launch
34create table public.user_devices (
35 id uuid primary key default gen_random_uuid(),
36 user_id uuid references auth.users(id) on delete cascade not null,
37 fcm_token text not null,
38 platform text not null check (platform in ('ios', 'android')),
39 timezone text not null default 'UTC',
40 updated_at timestamptz default now(),
41 constraint uq_user_fcm unique (user_id, fcm_token)
42);
43
44-- Leaderboard view (read-safe for all authenticated users)
45create or replace view public.v_leaderboard as
46 select
47 us.challenge_id,
48 us.user_id,
49 us.current_streak,
50 us.longest_streak,
51 row_number() over (partition by us.challenge_id order by us.current_streak desc) as rank
52 from public.user_streaks us
53 order by us.current_streak desc
54 limit 100;
55
56-- RLS
57alter table public.challenges enable row level security;
58alter table public.challenge_entries enable row level security;
59alter table public.user_streaks enable row level security;
60alter table public.user_devices enable row level security;
61
62-- Challenges: anyone authenticated can read; only admins/creators can insert
63create policy "Authenticated users can read challenges"
64 on public.challenges for select
65 using (auth.role() = 'authenticated');
66
67create policy "Creators can insert challenges"
68 on public.challenges for insert
69 with check (auth.uid() = created_by);
70
71-- Entries: own rows only for insert; own rows only for select
72create policy "Users can insert own entries"
73 on public.challenge_entries for insert
74 with check (auth.uid() = user_id);
75
76create policy "Users can read own entries"
77 on public.challenge_entries for select
78 using (auth.uid() = user_id);
79
80-- Streaks: own rows only for write; ALL rows readable for leaderboard
81create policy "Users can read all streaks (leaderboard)"
82 on public.user_streaks for select
83 using (true);
84
85create policy "System can upsert streaks"
86 on public.user_streaks for all
87 using (auth.uid() = user_id)
88 with check (auth.uid() = user_id);
89
90-- Devices: own rows only
91create policy "Users manage own devices"
92 on public.user_devices for all
93 using (auth.uid() = user_id)
94 with check (auth.uid() = user_id);
95
96-- Index for fast streak lookup and entry calendar queries
97create index entries_user_challenge_date_idx
98 on public.challenge_entries (user_id, challenge_id, entry_date desc);
99
100create index devices_user_idx
101 on public.user_devices (user_id);

Heads up: The user_streaks SELECT policy uses USING (true) so any authenticated user can read all participants' streaks — this is what powers the leaderboard. INSERT and UPDATE remain restricted to own rows. The streak_calculator Postgres function (not shown here) must run with security definer privileges to upsert into user_streaks on behalf of the triggering user.

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.

Native iOS + AndroidFit for this feature:

Best fit for this mobile-only feature. FlutterFlow's visual Flutter builder handles the calendar UI natively, wires Supabase queries visually, and configures FCM push through the Firebase settings panel — no separate SDK setup required.

Step by step

  1. 1Create a new FlutterFlow project; under Settings → Firebase, connect your Firebase project to enable FCM push notifications
  2. 2Add Supabase as the backend under Settings → Supabase and run the SQL schema from this page in the Supabase SQL Editor
  3. 3Build the challenge home page: add a horizontal ListView for the calendar strip, a Text widget bound to user_streaks.current_streak, and a fl_chart RadialBarChart for the progress ring — bind all three to Supabase queries
  4. 4Add a Button labeled 'Check In Today' with an Action: Supabase Insert into challenge_entries; add error handling via a Custom Action that catches Postgres error 23505 and shows a SnackBar 'Already checked in today'
  5. 5In the Action editor, wire up Firebase Messaging to request push permission on first launch and call FirebaseMessaging.instance.getToken(), then upsert the token into user_devices
  6. 6Configure the Supabase Edge Function for daily push reminders in the Supabase Dashboard under Edge Functions; schedule it via pg_cron under Database → Extensions

Where this path bites

  • The streak_calculator Postgres trigger and pg_cron schedule must be written directly in the Supabase SQL editor — FlutterFlow has no visual interface for Postgres functions or scheduled jobs
  • Leaderboard pagination beyond the first page requires a custom Dart action; FlutterFlow's built-in Supabase query does not expose range-based pagination natively
  • Code export for streak engine customization (grace periods, weighted scoring) requires the Pro plan at $70/mo
  • FCM v1 API in the Edge Function requires a Firebase service account JSON key — store it as a Supabase Secret, not hardcoded

Third-party services you'll need

The feature runs on three services. Two are free at launch scale; FCM only incurs cost above 100K messages per month.

ServiceWhat it doesFree tierPaid from
Firebase Cloud Messaging (FCM v1 API)Delivers push notifications to Android and iOS devices; called from the Supabase Edge Function with device tokens stored in user_devices100K messages/month free$0.01/1K messages above 100K (approx)
SupabasePostgreSQL database for all tables, Edge Functions for streak trigger and push scheduler, Realtime for live leaderboard updatesFree tier: 2 projects, 500MB DB, 500K Edge Function invocations/moPro: $25/mo (8GB DB, unlimited projects)
fl_chart (Flutter pub.dev package)Renders the circular progress ring (RadialBarChart) and any additional bar or line charts on the progress dashboardFree, open-source (BSD license)Free

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

$0/mo

Supabase free tier handles the DB, Realtime, and Edge Function invocations at this scale. FCM delivers push notifications within the 100K/month free tier even at 3 notifications/user/day. fl_chart has no cost.

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.

Streaks reset at the wrong time due to UTC vs local date mismatch

Symptom: Supabase stores entry_date as a DATE, which defaults to UTC. A user in UTC-8 who checks in at 11:00pm local time is actually past midnight UTC — their entry lands on the next UTC calendar day. The streak engine then sees a gap and resets the streak, even though the user checked in before their local midnight deadline. This is the most common complaint in challenge apps and impossible to debug without timezone logging.

Fix: Store entry_date in the user's local timezone. On the Flutter side, use DateTime.now().toLocal() and format as YYYY-MM-DD before inserting. Store the user's IANA timezone string (e.g. 'America/Los_Angeles') in user_devices.timezone. In the streak_calculator Postgres function, convert timestamps using AT TIME ZONE with the stored timezone string before comparing dates.

Duplicate check-in entries from double-tap or FlutterFlow retry

Symptom: FlutterFlow's 'Add Row' action can fire twice if the user double-taps the check-in button or if a slow network response triggers the automatic retry. Without a database constraint, this creates two entries for the same day, inflates completion counts, and makes the streak engine miscalculate.

Fix: The UNIQUE(challenge_id, user_id, entry_date) constraint in the schema above is the primary defense — Postgres will reject the second insert with error code 23505. Add a FlutterFlow Custom Action that catches this error and shows a SnackBar reading 'Already checked in today' instead of displaying a generic error. Also disable the check-in button immediately on first tap using a loading state variable.

FCM device tokens go stale after app reinstall

Symptom: When a user reinstalls the app, their FCM token changes. The old token remains in user_devices. The nightly Edge Function attempts delivery and FCM returns a 404 (Registration Not Found) or 410 (Unregistered) error. If the Edge Function swallows these errors silently, the user never receives reminders and the user_devices table accumulates dead tokens indefinitely.

Fix: On every app launch, call FirebaseMessaging.instance.getToken() in FlutterFlow's App State initialization and upsert the result into user_devices (using the unique constraint on user_id + fcm_token). In the Edge Function, inspect each FCM response: if the status is 404 or 410, delete that row from user_devices before proceeding to the next token.

Leaderboard returns empty for all users due to RLS blocking cross-user reads

Symptom: The default Supabase RLS pattern for user-owned data uses USING (auth.uid() = user_id). Applied to user_streaks, this means every user can only see their own row — the leaderboard query returns exactly one result or nothing. This looks like a data problem but is actually an RLS configuration issue.

Fix: Add a separate SELECT policy on user_streaks with USING (true) alongside the restricted INSERT/UPDATE policies. This lets any authenticated user read all streak rows (which is intentional — it powers the public leaderboard) while write operations remain restricted to own rows. The SQL schema in this page includes this policy.

Offline check-in sync creates backdated entries that break the streak display

Symptom: If FlutterFlow's local state caching is enabled and a user checks in while offline, the entry may be inserted with the current timestamp when connectivity returns — but if that's the next day, the date is off by one. The streak engine recalculates correctly based on the stored date, but the Supabase Realtime subscription may have already missed the event, leaving the leaderboard stale.

Fix: Require connectivity for check-ins: show a 'No internet connection — check in requires connectivity' banner when offline rather than queuing the entry. If offline-first is a product requirement, capture the check-in date at the moment of tap (not at sync time) and include it in the payload, then trigger a manual Realtime broadcast after the sync resolves.

Best practices

1

Store entry_date in the user's local timezone from the first line of code — retrofitting timezone awareness into a live app with existing streak data is a painful migration

2

Make the check-in button idempotent at the UI level (disable on first tap, re-enable after response) in addition to the database UNIQUE constraint — the constraint is the safety net, not the primary UX

3

Refresh FCM tokens on every app launch, not only on first install — token rotation on reinstall and app updates is silent and frequent

4

Show 'Last updated X minutes ago' on the leaderboard and add a manual refresh button — pg_cron-based refreshes run on a schedule, and users will assume stale data is a bug

5

Send push notifications in the user's local timezone using their stored timezone string — a reminder at 9pm UTC landing at 2am local destroys retention overnight

6

Cache the user's own streak in Flutter app state immediately after check-in before the Supabase response confirms — optimistic UI prevents the awkward delay between tap and counter update

7

Log every FCM delivery attempt and response code in an Edge Function execution log or a Supabase table — silent 404s from stale tokens are the hardest notification bugs to diagnose without logs

8

Scope leaderboards to the active challenge by default, not all-time global — a global leaderboard dominated by power users from day one discourages new participants from engaging

When You Need Custom Development

FlutterFlow covers the standard streak + leaderboard + push pattern well. These scenarios outgrow the visual builder:

  • Challenge rules are multi-phase or weighted — e.g. posting earns 1 point, posting with a photo earns 3, posting to two platforms earns 5 — binary check-in logic cannot model this without significant custom Dart code
  • Offline-first is a product requirement: users in areas with unreliable connectivity need check-ins to queue locally with SQLite (sqflite), sync when online, and resolve conflicts without double-counting
  • Enterprise or corporate wellness use case requiring SSO integration (SAML/OIDC), an admin dashboard with compliance exports, and audit trails that satisfy HR or legal
  • Auto-verification via wearables: connecting to Apple HealthKit or Google Fit to validate check-ins from step count, workout data, or heart rate means native SDK integration that FlutterFlow cannot express visually

RapidDev builds these features for production

Real auth, database, and payments — fixed price, delivered in weeks. You own 100% of the code.

Get a fixed-price quote

Frequently asked questions

Can I reset a user's streak from the admin panel?

Yes — add an admin-only Supabase RPC function that updates user_streaks.current_streak to 0 for a given user and challenge_id. Protect the function with a role check using auth.jwt() ->> 'role' = 'admin'. In FlutterFlow, wire an admin page with a user lookup and a 'Reset Streak' button that calls this RPC. Never modify streak rows directly from the app client — always go through a server-side function that can enforce the role check.

How do I prevent users from backdating their check-ins?

The entry_date is generated server-side or validated server-side — never trust the date sent by the client. In the Supabase Edge Function or Postgres trigger, compare the submitted entry_date against now() AT TIME ZONE user_timezone and reject any date that is not today or (if you allow it) yesterday within a grace period. Using a Postgres trigger with BEFORE INSERT means no client code path can bypass the validation.

What happens to the leaderboard when a challenge ends?

The leaderboard remains readable after the end_date — it shows final standings. Add a 'Challenge ended' banner by comparing the current date against challenges.end_date in your UI query. If you want to archive results, insert a snapshot of user_streaks into a challenge_results table at the end_date and query from there for historical views. This prevents ongoing app activity from polluting past challenge standings.

Can one user join multiple active challenges simultaneously?

Yes — the data model is keyed by (challenge_id, user_id), so a user can have independent streaks in as many challenges as they join. The UI should show a challenge picker on the home screen that switches the calendar strip and streak counter to the selected challenge's data. The leaderboard is also per-challenge, scoped by challenge_id in the query.

How do I send a reminder only to users who haven't checked in yet today?

In the Supabase Edge Function, run a query that finds all user_device rows where the user_id does NOT appear in challenge_entries for today's date in their local timezone. Use a LEFT JOIN or NOT EXISTS subquery: select devices.* from user_devices devices where not exists (select 1 from challenge_entries e where e.user_id = devices.user_id and e.entry_date = (now() AT TIME ZONE devices.timezone)::date). Then call FCM only for the returned device tokens.

Can the check-in require a photo upload as proof?

Yes — add a photo_url column to challenge_entries and create a Supabase Storage bucket for proof photos. In FlutterFlow, add an image picker action before the Supabase insert: upload the image to Storage, get the public URL, and include it in the insert payload. Add a NOT NULL constraint on photo_url for challenges where proof is mandatory. The calendar strip can show a thumbnail of the uploaded photo for completed days.

How do I show a streak flame animation that grows as the streak increases?

Use Flutter's AnimatedContainer or a Lottie animation file triggered when current_streak updates. In FlutterFlow, add a Lottie widget (Lottie.asset from the pub.dev package) bound to a page state variable for streak milestone thresholds — e.g. a small flame at 1-6 days, a medium flame at 7-29 days, a large flame at 30+ days. Trigger a confetti animation (using the confetti package) when the streak hits a milestone like 7, 30, or 100 days.

Is FlutterFlow good enough for this feature, or do I need a developer?

FlutterFlow handles the calendar UI, Supabase queries, FCM push wiring, and leaderboard display well — a non-technical founder can ship a complete challenge tracker in 1-2 days. The two things that require direct SQL editing are the streak_calculator Postgres trigger and the pg_cron schedule for the daily push job. Both are copy-paste operations in the Supabase Dashboard, not code you write from scratch. Custom development only becomes necessary if your challenge rules are more complex than binary check-in, you need offline-first with conflict resolution, or you require SSO and compliance exports.

RapidDev

Need this feature production-ready?

RapidDev builds a content challenge tracker into real apps — auth, database, payments — at $13K–$25K.

Book a free consultation

30-min call. No commitment.

Want this built for you?

We ship production apps at a fixed price — $13K–$25K, 6–10 weeks, source code yours. You've seen what it takes; we do it every week.

Get a fixed-price quote

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.