RORK LABJP
PRODUCT — Rork Max generates native Swift apps for iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and iMessageNATIVE — Rork Max unlocks AR/LiDAR, Metal 3D games, Dynamic Island, Live Activities, HealthKit, and Core MLCLASSIC — The original Rork uses React Native (Expo), turning plain-English prompts into shippable iOS/Android appsFUNDING — Rork raised $2.8M from a16z (plus $15M more), reaching 743,000 monthly visits at 85% growthPRICING — Rork is free to start, with paid plans from $25/month; Rork Max is $200/monthCHOICE — Pick cross-platform Rork or Rork Max for deep Apple-native capabilities, depending on your goalPRODUCT — Rork Max generates native Swift apps for iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and iMessageNATIVE — Rork Max unlocks AR/LiDAR, Metal 3D games, Dynamic Island, Live Activities, HealthKit, and Core MLCLASSIC — The original Rork uses React Native (Expo), turning plain-English prompts into shippable iOS/Android appsFUNDING — Rork raised $2.8M from a16z (plus $15M more), reaching 743,000 monthly visits at 85% growthPRICING — Rork is free to start, with paid plans from $25/month; Rork Max is $200/monthCHOICE — Pick cross-platform Rork or Rork Max for deep Apple-native capabilities, depending on your goal
Articles/Dev Tools
Dev Tools/2026-06-23Advanced

DAU Went Up but Retention Didn't — Rebuilding Gamification That Actually Sticks in Rork Apps

Points, badges, and leaderboards lift DAU, but retention is a different story. Field notes on a server-authoritative point ledger, streaks that forgive, and leaderboards that don't crush newcomers — with working code for Rork apps.

GamificationRetention11Rork442Supabase30React Native177Leaderboard

Premium Article

When I first added points, badges, and a leaderboard to one of my apps, daily active users did climb. Points popped on login, badges unlocked, and the numbers felt great. But a month later, when I lined up D7 retention by cohort, it had barely moved from before the launch. Worse, some users quietly drifted away the day after their streak broke.

The DAU bump came from the same people habitually tapping login — not from new users actually sticking around. As an indie developer running wallpaper and wellness apps, this was one of the more painful lessons I learned myself. Gamification is supposed to strengthen the relationship between a user and an app through enjoyment, but one wrong design turn and it just inflates vanity metrics.

This is a field-notes account of the three traps I hit after shipping, and how I rebuilt around them. I'll assume the basics — point-award hooks, badge checks — are already working, and dig into what separates gamification that works from gamification that merely looks busy.

Locally stored points are unmeasurable and tamperable

My first implementation kept points and streaks in AsyncStorage, locally. It's easy and fast, but two problems surface almost immediately in production.

The first is that you can't measure anything. If points live only on the device, the server has no record of which actions actually drive retention. Did the people who earned Badge A stick around longer at 30 days than those who earned Badge B? Without that comparison, gamification never enters an improvement loop.

The second is that it's trivially tampered with. If the score sent to the leaderboard originates on the client, anyone can rewrite it on-device. Even for a niche solo-developer app, a few prank scores at the top of the ranking will instantly deflate your honest users.

The conclusion: the source of truth for points belongs on the server. The client should send only the fact that "this action happened," and the server decides the score and the ranking.

Make the server-authoritative ledger idempotent

The thing most people stumble on here is double-counting the same action. On mobile, where networks are flaky, request retries are routine. If a tap gets sent twice, it gets credited twice. The key to preventing that is idempotency.

The client generates a unique ID per action, and the server records that ID in the ledger. If the same ID arrives again, the second time simply returns the current value without crediting anything.

A ledger table and an atomic award RPC

-- Run in the Supabase SQL Editor
-- 1) Point ledger (1 row = 1 transaction). idempotency_key blocks double credits
create table public.point_ledger (
  id uuid primary key default gen_random_uuid(),
  user_id uuid not null references auth.users(id) on delete cascade,
  action text not null,
  points integer not null,
  idempotency_key text not null,
  created_at timestamptz not null default now(),
  constraint uq_idem unique (user_id, idempotency_key)
);
 
-- 2) Aggregated scores (for the leaderboard)
create table public.user_scores (
  user_id uuid primary key references auth.users(id) on delete cascade,
  display_name text not null,
  total_points integer not null default 0,
  updated_at timestamptz not null default now()
);
 
-- 3) Server-authoritative award function. The point table lives on the server
create or replace function public.award_points(
  p_action text,
  p_idempotency_key text
) returns integer
language plpgsql
security definer
as $$
declare
  v_user uuid := auth.uid();
  v_points integer;
  v_total integer;
begin
  if v_user is null then
    raise exception 'not authenticated';
  end if;
 
  -- Point values are the server-side source of truth
  v_points := case p_action
    when 'daily_login'   then 10
    when 'complete_task' then 25
    when 'share_content' then 15
    when 'invite_friend' then 50
    else 0
  end;
 
  if v_points = 0 then
    raise exception 'unknown action: %', p_action;
  end if;
 
  -- Idempotency: if the key already exists, return current total without crediting
  begin
    insert into public.point_ledger(user_id, action, points, idempotency_key)
    values (v_user, p_action, v_points, p_idempotency_key);
  exception when unique_violation then
    select total_points into v_total from public.user_scores where user_id = v_user;
    return coalesce(v_total, 0);
  end;
 
  -- Update the aggregate atomically in one statement (avoids read-then-write races)
  insert into public.user_scores(user_id, display_name, total_points)
  values (v_user, coalesce((auth.jwt() ->> 'name'), 'Player'), v_points)
  on conflict (user_id)
  do update set total_points = public.user_scores.total_points + excluded.total_points,
                updated_at = now()
  returning total_points into v_total;
 
  return v_total;
end;
$$;

The points are incremented in a single statement with RETURNING. If you instead "read the current value in the app, add, and write it back," one increment vanishes whenever two devices or requests run at the same time. Let the database do the arithmetic — it's the safe choice.

The client sends only the fact and the key

// lib/awardPoints.ts — ask the server to award points with an idempotency key
import * as Crypto from 'expo-crypto';
import { supabase } from './supabase';
 
type Action = 'daily_login' | 'complete_task' | 'share_content' | 'invite_friend';
 
export async function awardPoints(action: Action): Promise<number | null> {
  // A unique key per action (retries produce the same result)
  const idempotencyKey = Crypto.randomUUID();
 
  const { data, error } = await supabase.rpc('award_points', {
    p_action: action,
    p_idempotency_key: idempotencyKey,
  });
 
  if (error) {
    // On failure you can safely retry with the same key (no double credit)
    console.warn('award_points failed:', error.message);
    return null;
  }
  return data as number; // the server-confirmed total
}
 
// Usage:
// const total = await awardPoints('complete_task');
// total is the updated sum. The UI trusts this value to render

One nuance worth calling out: for daily_login you'll want the key to be valid only once per day. A pure random UUID would let someone earn the login bonus repeatedly in a single day, so for daily_login I use a deterministic key like `daily_login:${todayYYYYMMDD}`, and let the second attempt fail with unique_violation.

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
Why locally stored points are both unmeasurable and tamperable, and how to build an idempotent server-authoritative ledger with a Supabase RPC
How to compute forgiving streaks server-side with grace days (streak freeze) so a single missed day doesn't push users away
Why a global leaderboard discourages new users, and how relative leagues plus an around-me window fix it
Secure payment via Stripe · Cancel anytime

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

Related Articles

Dev Tools2026-04-19
Offline-First Architecture in Rork Apps: WatermelonDB + Supabase Sync
A complete guide to implementing offline-first architecture in Rork apps using WatermelonDB and Supabase Realtime. Covers local caching, optimistic updates, conflict resolution, and cross-device sync.
Dev Tools2026-04-07
Rork Max × Liveblocks / Yjs: Real-Time Collaborative App Development
A complete guide to integrating Liveblocks and Yjs into Rork Max apps for real-time collaborative editing. From CRDT fundamentals to production deployment, everything you need to build multi-user apps.
Dev Tools2026-04-02
Building a Survey App with Rork — A Beginner's Guide to Forms, Data Collection, and Charts
Learn how to build a survey collection app from scratch using Rork. This beginner-friendly guide covers form design, multiple answer types, Supabase data storage, and aggregation charts — all in a single day.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →