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

How to Add Gamification to Your App (Copy-Paste Prompts Included)

Gamification needs six pieces: an XP counter reading from Supabase with Realtime for instant updates, a badge gallery with locked/unlocked states, a streak widget with daily UTC reset, a points engine Edge Function triggered by user actions, a badge evaluation function, and a pg_cron job for streak resets. With Lovable you can ship a working system in 6-9 hours for $0/month — costs only appear past 1,000 users when Supabase Pro is needed.

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

Feature spec

Intermediate

Category

analytics-admin

Build with AI

6-10 hours with Lovable or v0 (phased build)

Custom build

2-4 weeks custom dev

Running cost

$0/mo up to 1,000 users; $25-50/mo at 10,000 users

Works on

Web

Everything it takes to ship Gamification — parts, prompts, and real costs.

TL;DR

Gamification needs six pieces: an XP counter reading from Supabase with Realtime for instant updates, a badge gallery with locked/unlocked states, a streak widget with daily UTC reset, a points engine Edge Function triggered by user actions, a badge evaluation function, and a pg_cron job for streak resets. With Lovable you can ship a working system in 6-9 hours for $0/month — costs only appear past 1,000 users when Supabase Pro is needed.

What a Gamification System Actually Is

Gamification adds game-like mechanics — XP points, badges, level progression, and daily streaks — to a non-game product to increase engagement and retention. The product decisions are: which user actions award points and how many, what badges exist and what unlocks them, how levels scale (linear or exponential XP thresholds), and whether the gamification is private (personal progress) or public (visible on a leaderboard). The visual components are straightforward — a progress bar, a badge grid, a flame icon. The complexity lives in the event-driven backend: making the XP counter update the instant a user completes a lesson, preventing double-awarding badges, and running the streak reset at exactly midnight UTC every night.

What users consider table stakes in 2026

  • XP or points counter visible in the app header or profile section, updating instantly when a qualifying action is completed
  • Badge gallery showing all available badges with locked (greyed out) and unlocked states, and the earn condition visible on hover or tap
  • Streak counter showing current streak number, longest streak, and a countdown timer to the next midnight UTC reset
  • Level progress bar between the current and next level, showing XP needed to level up numerically
  • Celebratory micro-animation on milestone achievement — confetti burst, badge pop-up, or level-up banner
  • Toast notification or in-app alert when a new badge is earned or a level is reached — without it, users miss the reward entirely

Anatomy of the Feature

Six components. Lovable handles the Supabase schema and badge gallery on a first prompt. The pg_cron streak reset, canvas-confetti animation, and complex badge conditions reliably need a second pass.

Layers:UIBackend

XP counter + level bar

UI

React component that reads xp and level from the user_gamification Supabase table via a Realtime subscription. shadcn/ui Progress component for the level bar filled by ((xp - current_level_min) / (next_level_min - current_level_min)) * 100 — using level breakpoints from a levels reference table. canvas-confetti npm package (MIT, free, 7KB) fires the celebration burst on level-up.

Note: Calculate level bar fill using the current and next level's XP minimums from a levels table rather than raw xp — otherwise the bar shows > 100% immediately after a level-up before the level value increments.

Badge gallery

UI

CSS Grid of badge cards reading from a badges reference table (all available badges) joined with user_badges (earned badges). Unlocked badges render at full opacity with a checkmark; locked badges use Tailwind opacity-50 with a lock icon. The earn condition text (e.g. 'Complete 5 lessons') is always visible so users understand what to do next.

Note: Store badge metadata (name, icon URL, earn condition text) in the badges table, not in frontend code — this lets you add new badges without a code deploy.

Streak widget

UI

Reads current_streak and longest_streak from the user_streaks table. A countdown timer to midnight UTC is calculated with dayjs (dayjs.utc().endOf('day').diff(dayjs.utc(), 'second')) and displayed as 'Resets in 4h 22m'. Heroicons flame icon shifts from grey to orange at streak >= 3 and to red at streak >= 7.

Note: Use dayjs instead of the native Date API for UTC-aware countdown calculations — new Date().setHours(23,59,59) uses the user's local timezone, not UTC.

Event listener / points engine

Backend

Supabase Edge Function (award_points) called from the client or from a Postgres trigger after qualifying actions. Receives event_type and user_id, looks up the point value in an event_point_values config table, increments user_gamification.xp, checks level thresholds, updates level if crossed, inserts into user_xp_log for audit, and updates user_streaks.last_active_date + increments current_streak if the date is new.

Note: Keep point values in a Supabase config table (event_type, points_awarded) rather than hardcoded in the Edge Function — changing point values then requires a table update, not a code deployment.

Badge evaluation engine

Backend

Supabase Edge Function (check_badges) called after award_points completes. Iterates the badges table, evaluates each badge's condition_type and condition_value against the user's current stats (total XP, streak length, specific action counts from user_xp_log), and inserts into user_badges with ON CONFLICT (user_id, badge_id) DO NOTHING for any newly earned badges. Emits a Supabase Realtime broadcast on channel 'badges:{user_id}' so the frontend can show the toast notification.

Note: ON CONFLICT DO NOTHING is the critical safeguard — without it, concurrent requests (e.g. completing two actions rapidly) can race past the check and award the same badge twice.

Streak reset cron

Backend

Postgres pg_cron job running daily at 00:05 UTC. UPDATE user_gamification SET current_streak = 0, last_active_date = NULL WHERE last_active_date < CURRENT_DATE - 1. Preserves longest_streak if current_streak was higher before reset. The 5-minute offset (00:05 not 00:00) avoids clock-edge race conditions between the cron and the award_points Edge Function.

Note: pg_cron must be manually enabled in Supabase Dashboard → Database → Extensions before the schedule call will work. The free tier supports it.

The data model

Five tables: the main gamification state per user, a badges reference catalogue, earned badges, an XP audit log, and a levels reference table for breakpoints. Paste this into the Supabase SQL Editor and run it before prompting your AI tool:

schema.sql
1create table public.levels (
2 level int primary key,
3 xp_required int not null,
4 title text
5);
6
7insert into public.levels (level, xp_required, title) values
8 (1, 0, 'Beginner'),
9 (2, 100, 'Explorer'),
10 (3, 300, 'Achiever'),
11 (4, 600, 'Expert'),
12 (5, 1000, 'Master');
13
14create table public.user_gamification (
15 user_id uuid primary key references auth.users(id) on delete cascade,
16 xp int not null default 0,
17 level int not null default 1 references public.levels(level),
18 current_streak int not null default 0,
19 longest_streak int not null default 0,
20 last_active_date date
21);
22
23create table public.badges (
24 id uuid primary key default gen_random_uuid(),
25 slug text not null unique,
26 name text not null,
27 description text,
28 icon_url text,
29 condition_type text not null,
30 condition_value int not null
31);
32
33create table public.user_badges (
34 user_id uuid references auth.users(id) on delete cascade not null,
35 badge_id uuid references public.badges(id) on delete cascade not null,
36 earned_at timestamptz not null default now(),
37 primary key (user_id, badge_id)
38);
39
40create table public.user_xp_log (
41 id uuid primary key default gen_random_uuid(),
42 user_id uuid references auth.users(id) on delete cascade not null,
43 action text not null,
44 xp_awarded int not null,
45 created_at timestamptz not null default now()
46);
47
48-- RLS
49alter table public.user_gamification enable row level security;
50alter table public.user_badges enable row level security;
51alter table public.user_xp_log enable row level security;
52
53create policy "Users read own gamification state"
54 on public.user_gamification for select
55 using (auth.uid() = user_id);
56
57create policy "Users update own gamification state"
58 on public.user_gamification for update
59 using (auth.uid() = user_id);
60
61create policy "Users insert own gamification row"
62 on public.user_gamification for insert
63 with check (auth.uid() = user_id);
64
65create policy "Badges are public"
66 on public.badges for select
67 using (true);
68
69create policy "Users read own badges"
70 on public.user_badges for select
71 using (auth.uid() = user_id);
72
73create policy "Users read own xp log"
74 on public.user_xp_log for select
75 using (auth.uid() = user_id);
76
77-- Indexes
78create index user_xp_log_user_idx on public.user_xp_log (user_id, created_at desc);
79create index user_xp_log_action_idx on public.user_xp_log (user_id, action);
80
81-- pg_cron streak reset at 00:05 UTC daily
82-- (run after enabling pg_cron extension in Dashboard Database Extensions)
83select cron.schedule(
84 'streak-reset',
85 '5 0 * * *',
86 $$
87 update public.user_gamification
88 set
89 longest_streak = greatest(longest_streak, current_streak),
90 current_streak = 0,
91 last_active_date = null
92 where last_active_date < current_date - 1
93 $$
94);

Heads up: Seed the levels table with your actual XP breakpoints before launching — users will see 'Level 1' forever if the levels table is empty and the Edge Function cannot find a matching next-level threshold.

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.

Full-stack, non-technical friendlyFit for this feature:

Best all-round path: Lovable generates the full Supabase schema, award_points and check_badges Edge Functions, and the React badge gallery and XP bar in one or two prompts. Supabase Realtime for live XP updates and badge toasts works out of the box with the right prompt.

Step by step

  1. 1Create a Lovable project with Lovable Cloud enabled and paste the first prompt below for the schema and XP system
  2. 2Verify the award_points Edge Function exists in Supabase Dashboard → Edge Functions and test it by invoking it with a test user_id and action
  3. 3Enable pg_cron in Supabase Dashboard → Database → Extensions and run the cron schedule SQL from this page
  4. 4Paste the second prompt (or add to the first) specifying the badge gallery and the canvas-confetti celebration animation
  5. 5Test the complete flow: complete a qualifying action, verify XP increments in user_gamification, verify the badge gallery shows the new earned badge, and verify the confetti fires once and not on re-renders
Paste into Lovable
Build a gamification system for a web app using Supabase. Create these tables: levels (level int pk, xp_required int, title text — seed with: level 1=0 XP Beginner, level 2=100 XP Explorer, level 3=300 XP Achiever, level 4=600 XP Expert, level 5=1000 XP Master), user_gamification (user_id uuid pk fk auth.users, xp int default 0, level int default 1 fk levels, current_streak int default 0, longest_streak int default 0, last_active_date date), badges (id uuid pk, slug text unique, name text, description text, icon_url text, condition_type text, condition_value int), user_badges (user_id fk, badge_id fk, earned_at, pk user_id+badge_id), user_xp_log (id pk, user_id fk, action text, xp_awarded int, created_at). RLS: user_gamification SELECT/UPDATE/INSERT own row; badges SELECT public; user_badges SELECT own rows. Create a Supabase Edge Function called award_points that: receives {user_id, action} in the request body; looks up points for the action from this map: lesson_complete=10, review_posted=5, daily_login=5, challenge_won=50; upserts a user_gamification row (ON CONFLICT user_id DO UPDATE SET xp = xp + points, last_active_date = today, current_streak = CASE WHEN last_active_date = yesterday THEN current_streak + 1 ELSE 1 END, level = (SELECT level FROM levels WHERE xp_required <= xp+points ORDER BY level DESC LIMIT 1)); inserts into user_xp_log; then calls check_badges. Create a Supabase Edge Function check_badges that: queries all badges, evaluates condition_type 'min_xp' against user_gamification.xp and condition_type 'min_streak' against current_streak; inserts into user_badges ON CONFLICT (user_id, badge_id) DO NOTHING for any newly earned badges; broadcasts on Supabase Realtime channel 'badges:{user_id}' with the newly earned badge data. UI: a GamificationWidget showing: XP counter (current XP / next level XP required from levels table), shadcn/ui Progress bar filled by ((xp - currentLevelMin) / (nextLevelMin - currentLevelMin)) * 100, streak flame icon (grey <3, orange >=3, red >=7) with countdown to midnight UTC using dayjs, and a level badge showing level number. On award_points response: if level increased, fire canvas-confetti; subscribe to 'badges:{userId}' Realtime channel and show a shadcn/ui Toast with the badge name and icon on each broadcast. Badge gallery page: CSS Grid of all badges from the badges table, left-joined with user_badges; locked badges show at opacity-50 with a lock icon; unlocked show with a gold checkmark and earned_at date.

Where this path bites

  • pg_cron streak reset must be enabled manually in Supabase Dashboard → Database → Extensions — the Edge Function covers daily login detection but the cron reset for inactive users requires the extension
  • canvas-confetti animation often requires a follow-up prompt to wrap in useEffect with a one-shot flag — without it, confetti fires on every React re-render
  • Complex badge conditions (e.g. 'complete 3 different lesson types in one week') need explicit condition_type definitions in the prompt — the AI simplifies to min_xp and min_streak by default

Third-party services you'll need

Gamification runs entirely on Supabase plus free client-side libraries. Analytics integration is optional:

ServiceWhat it doesFree tierPaid from
SupabaseGamification state DB, Edge Functions for award_points and check_badges, Realtime broadcasts for badge toasts, pg_cron for streak resetFree tier: 500MB DB, 2 projects, 500K Edge Function invocations/moPro $25/mo (8GB DB, higher Edge Function and Realtime limits)
canvas-confettiLevel-up and badge celebration animation — fires a burst of confetti particles on milestone achievementMIT license, free, 7KB minifiedFree
Amplitude (optional)Track gamification events (badge earned, level up, XP awarded) for product analytics and funnel analysisStarter plan free up to 10 million events/month (approx)Growth plan — contact Amplitude for pricing

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 covers storage and Edge Function invocations easily. canvas-confetti is client-side and free. No third-party analytics needed at this scale.

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.

XP counter updates in the database but the UI stays stale

Symptom: The Supabase Realtime subscription is created inside the React component but the channel is subscribed to the wrong table name or wrong filter. The AI often subscribes to a channel named 'gamification' but the table is named 'user_gamification', or subscribes without the user_id filter — so all users' updates broadcast to all clients.

Fix: Confirm the Realtime subscription target exactly: supabase.channel('xp-update').on('postgres_changes', { event: 'UPDATE', schema: 'public', table: 'user_gamification', filter: `user_id=eq.${userId}` }, callback).subscribe(). Check Supabase Dashboard → Realtime → Inspector to verify the channel is receiving events.

The same badge is awarded twice or more

Symptom: check_badges is called multiple times in parallel when a user completes two actions in rapid succession. Both Edge Function invocations read the user's stats before either has written the new badge row, so both pass the badge condition check and both attempt to insert the badge — creating duplicate rows.

Fix: Add INSERT INTO user_badges (user_id, badge_id, earned_at) VALUES ($1, $2, now()) ON CONFLICT (user_id, badge_id) DO NOTHING to the Edge Function. The PRIMARY KEY (user_id, badge_id) is the authoritative guard — even concurrent inserts will have one succeed and one silently do nothing.

Streak resets for users who were active today

Symptom: pg_cron is enabled but the timezone offset in the cron expression or the comparison logic is wrong. Users in UTC-5 who complete a lesson at 8 PM local time (01:00 UTC next day) lose their streak because last_active_date was written as yesterday in UTC even though it was today locally.

Fix: Always store last_active_date as UTC in the Edge Function: new Date().toISOString().split('T')[0]. Document in the streak widget UI that 'Streak resets at midnight UTC' — this is the single most important user-facing communication to prevent support tickets.

Level progress bar shows over 100% or jumps to 0 after leveling up

Symptom: The level bar calculates fill as (xp / next_level_threshold) * 100. After leveling up, xp is not reset — it continues accumulating. So immediately after reaching level 2 (100 XP), a user with 150 XP shows 150% on the level 2 bar. The bug is using raw XP instead of XP within the current level.

Fix: Calculate fill as: ((xp - currentLevel.xp_required) / (nextLevel.xp_required - currentLevel.xp_required)) * 100. Fetch both the current level row and the next level row from the levels table in the component. Cap the result at 100 with Math.min(fillPct, 100) to handle edge cases.

canvas-confetti fires on every React re-render

Symptom: The confetti call is placed directly in the component body or in a useEffect without a one-shot flag. Every time the parent component re-renders — including on unrelated state changes — the confetti fires again, flooding the screen and annoying users.

Fix: Wrap in useEffect with a one-shot boolean: useEffect(() => { if (justLeveledUp) { confetti({ particleCount: 120, spread: 70 }); setJustLeveledUp(false); } }, [justLeveledUp]). The setJustLeveledUp(false) immediately after firing prevents re-runs on subsequent renders.

Best practices

1

Store point values in a Supabase config table (event_type TEXT, points_awarded INT) rather than hardcoding them in the Edge Function — changing reward values then requires a database update, not a code deployment

2

Award badges with ON CONFLICT (user_id, badge_id) DO NOTHING — this is the only reliable way to prevent duplicate badges under concurrent invocations

3

Calculate level bar fill using current and next level XP minimums from the levels table, not raw XP — raw XP breaks the display immediately after a level-up

4

Run the streak reset cron at 00:05 UTC, not exactly 00:00, to avoid clock-edge race conditions with the award_points Edge Function that might fire at exactly midnight

5

Always show the earn condition text for locked badges — 'Complete 5 lessons' is the motivation to return; a greyed-out badge with no explanation is a dead end

6

Subscribe to Supabase Realtime on 'badges:{user_id}' (user-scoped channel) rather than a global channel — broadcasting badge events to all users is both a data privacy concern and a performance waste

7

Test the badge evaluation function with a seeded user_xp_log that triggers every badge condition before launch — in production, you cannot un-award a badge without a manual database operation

8

Document the streak reset timezone (UTC) prominently in the streak widget — this generates more support tickets than any other gamification edge case

When You Need Custom Development

Lovable and v0 handle fixed-rule gamification — predefined XP values, a static badge catalogue, and a linear level system — very well. You outgrow them when the rules themselves need to be dynamic:

  • Badge creation and XP rule editing must be available to non-technical admins without a code deploy — requires a full rule engine CMS with condition builder UI
  • Seasonal or time-limited badges with availability windows (e.g. 'Earn by Dec 31') need automated activation and deactivation cron jobs and an admin scheduler
  • Gamification data must sync bidirectionally with an external CRM (HubSpot, Salesforce) or marketing automation tool to trigger lifecycle emails based on badge status
  • Anti-exploit logic must detect and penalise suspicious XP accumulation patterns in real time — bot-like scoring velocity above 3 standard deviations from user cohort mean

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

How many XP actions are too many — will users game the system?

Start with 4-6 action types maximum. More than that and users optimise for the easiest action rather than the most valuable one. Add server-side rate limiting (maximum 1 award per action type per 60 seconds) to prevent click-farming. Check user_xp_log for velocity anomalies in the first week after launch.

Can I let admins create new badges without a developer?

With Lovable or v0, no — the badges table has rows but adding a new badge requires a database INSERT, which is a developer task unless you build an admin UI. For admin-editable badges, build a protected /admin/badges page that lets your team fill a form that inserts rows into the badges table via a Server Action.

How do I handle users who change timezones and affect their streak?

Store and evaluate streaks entirely in UTC — never use the user's local timezone for streak calculation. Document in the streak widget: 'Streaks reset at midnight UTC'. If a user travels and their 'day' starts at a different time, the UTC reset is the authoritative clock. Trying to personalise reset time per timezone creates edge cases that are not worth the engineering cost for most apps.

Can I add a public leaderboard alongside the badge system?

Yes — the user_gamification.xp column is the score source. Add a leaderboard_view materialized view with RANK() OVER (ORDER BY xp DESC) and refresh it via pg_cron every 5 minutes. The Leaderboard feature page on this site covers the full implementation including the current-user sticky row.

How do I make the badge pop-up feel satisfying without being annoying?

Show the toast notification for 4 seconds, not indefinitely. Play a subtle sound if your app has audio (an HTML5 Audio element, 50ms clip). Fire canvas-confetti with 80-120 particles and a 0.6-second duration — longer feels gratuitous. Never show more than one badge notification at a time; queue them if multiple badges are earned simultaneously.

Will gamification slow down my app if every action triggers a database write?

The award_points Edge Function adds roughly 50-150ms to the action that triggers it. For actions where speed matters (e.g. a real-time game tap), call award_points asynchronously after the main action completes — fire and forget with no await. For actions where the XP display matters immediately (e.g. lesson completion), await the Edge Function and update the UI with the response.

How do I run a limited-time double-XP event?

Add a multiplier column to the event_point_values config table with a default of 1.0. Add a multiplier_active_until TIMESTAMPTZ column. In award_points, before calculating points, check if multiplier_active_until > now() and multiply the base points by the multiplier value. To start a double-XP event, update the row: SET multiplier = 2.0, multiplier_active_until = '2026-12-31 23:59:59Z'.

Can users see each other's badges on their profiles?

The current RLS policy allows users to read only their own user_badges rows. To make badges public on profile pages, change the SELECT policy to: using (true). Fetch another user's badges by querying user_badges WHERE user_id = profileUserId — no auth.uid() filter. Make sure the badge icon URLs are publicly accessible (Supabase Storage public bucket or an external CDN).

RapidDev

Need this feature production-ready?

RapidDev builds gamification 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.