How to Build a Leaderboard for Your App
Apps on Trophy's platform that add social leaderboards to their streak features see average streak length climb from 4.25 to 5.69 days — a 34% lift. That number holds across app categories, which tells you leaderboards aren't just a cosmetic social layer. When users can see how they rank against people they care about, they actually stay longer.

| Avg streak length (days) | P75 streak length (days) | P95 streak length (days) | |
|---|---|---|---|
| With social leaderboards | 5.69 | 7.00 | 12.00 |
| Without social leaderboards | 4.25 | 5.00 | 7.00 |
Source: Trophy platform data, April 2026. Average and percentile streak length for users with streaks longer than two days, comparing apps that use Trophy's social streak leaderboard feature against those that do not.
Most teams building leaderboards for the first time don't reach for consumer-app infrastructure. They search "how to build a leaderboard" and land in game developer territory — Firebase Game Services, Unity Leaderboards, PlayFab. Those tools were designed for session-based multiplayer games: discrete rounds, fixed scoring windows, player-vs-player match results.
Consumer apps don't work that way. Your users accumulate activity continuously, compete across time zones, sync from offline or wearable devices, and expect ranked competitions that reset and stay winnable. Different patterns, different infrastructure.
Here's what Trophy has learned from running leaderboards across consumer apps, where the naive implementations break and what the production-ready approach actually looks like.
The Approach That Works in Development
Most engineers start with a sorted query against their main database. It's simple to reason about and quick to prototype:
// A straightforward leaderboard query — fine at small scale
async function getLeaderboardRankings(
limit: number = 10
): Promise<LeaderboardEntry[]> {
const rows = await db.query(
`SELECT user_id, SUM(value) as total
FROM user_activity
GROUP BY user_id
ORDER BY total DESC
LIMIT $1`,
[limit]
);
return rows.map((row, index) => ({
userId: row.user_id,
rank: index + 1,
value: Number(row.total),
}));
}
This gets you a working prototype in under an hour. It serves the demo well. The problems emerge when real users arrive.
Where This Breaks for Consumer Apps
The global ranking problem. A consumer fitness app with 50,000 users showing a global leaderboard means only a few dozen people — those genuinely competing for the top spots — have any reason to engage with it. User 32,849 sees their rank, concludes they'll never move meaningfully in either direction, and ignores the feature.
Perpetual leaderboards go stale. The users who joined earliest accumulate the most activity. By month two, the top 20 spots on a perpetual board are locked in by power users. New sign-ups (the users you most need to retain) arrive, see they're 3,000 points behind the leader with no realistic path to the top, and disengage from the leaderboard immediately. Perpetual boards work for all-time hall-of-fame displays; they don't work as the primary engagement mechanic.
Late data corrupts rankings. Unlike a game server where you control when activity is submitted, consumer apps deal with offline sync and wearable uploads. A user completes a run on Monday morning but their watch syncs the workout on Tuesday afternoon. If your leaderboard ran from Monday to Sunday, that activity now arrives after the period closed. Without a way to handle late submissions, or to delay finalisation until all reasonable sync windows have closed, users will legitimately complain that their activity didn't count. This is one of the most common support tickets on consumer app leaderboards.
No rank-change hooks means no re-engagement. The most valuable moment in a competitive leaderboard isn't when a user is winning — it's when they've just been overtaken. That's the precise trigger for pulling someone back to the app. A bare database query against a generic activity log has no concept of rank transitions; you're on the hook for polling, diffing previous state, and building the notification pipeline yourself. That's months of work for what should be a simple task.
Time zone fairness collapses. A weekly leaderboard that resets at midnight UTC gives users in London a fair window, but shortchanges users in Los Angeles (who lose five hours of their Monday) and disadvantages users in Tokyo in the opposite direction. For a game where all sessions happen in real time, UTC is fine. For a consumer habit app where activity is spread throughout the user's local day, you need per-user timezone tracking and a finalisation window that waits for the last timezone to finish before declaring a winner.
The Trophy Approach
Trophy's leaderboard infrastructure is built specifically for the consumer app patterns above. Integration works in three parts.
Track activity and get rankings back immediately
Send a metric event when a user completes an action. The response includes their current leaderboard positions — no separate ranking query required:
import { TrophyApiClient } from '@trophyso/node';
const trophy = new TrophyApiClient({ apiKey: process.env.TROPHY_API_KEY });
async function handleWorkoutComplete(userId: string, userTimezone: string) {
const response = await trophy.metrics.event('workouts_completed', {
user: {
id: userId,
tz: userTimezone, // e.g. 'America/Los_Angeles'
},
value: 1,
});
// Leaderboard positions are returned inline — no extra round trip
const position = response.leaderboards?.['weekly-workouts'];
if (position) {
console.log(`User is now rank ${position.rank} of ${position.total}`);
}
return response;
}
The tz field is how Trophy tracks per-user timezones. Once set, all time-windowed leaderboard periods are calculated relative to the user's local time — the late-data and fairness problems above are handled at the infrastructure level.
Fetch a segmented leaderboard
Trophy's breakdown attributes let you partition users into smaller competitive groups by any attribute you pass at identify time — gym, city, skill tier, cohort, anything. You define the attributes; Trophy handles the partitioning. To fetch a leaderboard for a specific segment:
// Identify the user with their attributes when they sign up or update profile
await trophy.users.identify(userId, {
name: 'Jo Watkins',
attributes: {
city: 'london',
skill_level: 'intermediate',
},
});
// Fetch the leaderboard for a specific segment
async function getCityLeaderboard(city: string) {
const leaderboard = await trophy.leaderboards.get('weekly-workouts', {
offset: 0,
limit: 10,
userAttributes: `city:${city}`,
});
return leaderboard.rankings.map(entry => ({
userId: entry.userId,
rank: entry.rank,
value: entry.value,
}));
}
Each user is automatically entered into the leaderboards matching their attributes. A user with city: 'london' and skill_level: 'intermediate' can appear on both a London leaderboard and an intermediate-tier leaderboard simultaneously, with no additional event tracking required.
Trigger re-engagement on rank changes
Trophy fires a leaderboard.rank_changed webhook whenever a user's position shifts. Wire this to your push notification or email pipeline to pull users back when they've been overtaken:
// In your webhook handler
app.post('/webhooks/trophy', async (req, res) => {
const event = req.body;
if (event.type === 'leaderboard.rank_changed') {
const { userId, leaderboardKey, rank, previousRank } = event.data;
// User has dropped — trigger a re-engagement push
if (rank > previousRank) {
const positionsLost = rank - previousRank;
await sendPushNotification(userId, {
title: "You've been overtaken",
body: `You dropped ${positionsLost} place${positionsLost > 1 ? 's' : ''} this week. Open the app to defend your rank.`,
});
}
}
res.sendStatus(200);
});
Trophy also fires leaderboard.finished at the end of each period — useful for announcing weekly winners and showing recaps.

Three Decisions That Determine Whether Leaderboards Help or Hurt
The code above handles the implementation. These design decisions determine the outcome.
Repeating vs perpetual. Trophy's default recommendation for retention-focused leaderboards is repeating with a weekly period. Weekly boards give every user (including new sign-ups) a fair chance from the start of each run. Perpetual boards serve a specific purpose (all-time rankings, hall-of-fame status) but shouldn't be your primary competitive mechanic for the first year of a consumer app. The goal is to maximise the number of users who believe they can win, not to reward longevity of tenure.
Global vs segmented. The 34% streak lift Trophy sees with social leaderboards comes specifically from segmented, socially-connected competition — not global rankings. The effect is driven by users competing against peers they can identify with (same city, same gym, same cohort). A global leaderboard of all users typically shows a much weaker effect, because most users correctly perceive they have no realistic chance of placing. As a rule of thumb: if your app has more than a few thousand active users on a single leaderboard, segment it. The right breakdown attributes depend on your app — region and skill level are common starting points.
Metric vs points ranking. Metric leaderboards rank users on a single tracked action (workouts completed, lessons finished, miles run). Points leaderboards rank users on a Trophy points balance, which can reflect multiple behaviors — a combination of streaks completed, achievements unlocked, and volume logged. Use metric rankings when one action defines success in your app. Use points rankings when you want to reward breadth of engagement, not just volume of a single activity.
FAQ
What's the difference between a consumer app leaderboard and a game leaderboard?
Game leaderboards are typically session-scoped — they rank outcomes of discrete rounds or matches. Consumer app leaderboards track ongoing behaviour over days or weeks: habits, streaks, cumulative activity. This changes the infrastructure requirements significantly. Game leaderboard tools (Firebase Game Services, Unity Leaderboards, PlayFab) don't handle per-user timezone processing, continuous activity aggregation, or the mobile sync delays that are standard in consumer app data. Trophy is built for the consumer app pattern.
Should I start with a global leaderboard or go straight to segmentation?
Start segmented if you can. Even a simple split — by cohort week, acquisition channel, or self-reported skill level — produces more engagement than a global board, because it makes the competition feel winnable. If you don't have a natural attribute to segment on at launch, use repeating time windows as the primary fairness mechanism and add breakdown attributes once you understand your user base better.
When should a leaderboard use points ranking instead of metric ranking?
Use points ranking when you want to reward multiple types of engagement rather than volume of a single action. A language learning app might give points for lessons completed, streaks maintained, and achievement milestones — a points leaderboard rewards the well-rounded user, not just the one who mass-completes the easiest lesson type. Metric ranking is better when one action is clearly the core behaviour you want to reinforce.
How does Trophy handle users whose activity syncs late?
Trophy finalises leaderboard periods approximately 12 hours after the period ends in UTC. This window is designed to allow users across all time zones to complete their period and have late-arriving data (from offline sync or wearable uploads) included. Activity that arrives after finalisation is included in the next run rather than retroactively adjusting closed results.
What happens when a leaderboard fills up and a new user tries to join?
Trophy supports a maximum participant limit per leaderboard (or per breakdown group when using segmentation). When a leaderboard is full, a new user must exceed the current lowest-ranked participant's score to enter — at which point they displace that participant. This is handled automatically. You can surface this threshold in your UI ("Score 15 workouts this week to enter the leaderboard") to give users a specific goal.
Do I need to manage separate leaderboards for each city or region manually?
No. Once you configure a breakdown attribute in Trophy's dashboard, the platform automatically creates and manages the segment groups as users are identified with that attribute. Adding a new city doesn't require a database migration or any code change — users identifying with city: 'berlin' will automatically enter a Berlin-scoped leaderboard run. The participant limit applies at the group level, not globally.
Where to Go Next
If the design decisions behind segmentation are what you're working through — when to split a global board, how small to make the groups, what Strava's approach actually gets right — the analysis is in How Strava Uses Segmented Leaderboards to Drive Engagement.
For the complete implementation reference including all API parameters, webhook payloads, and leaderboard configuration options, the Trophy Leaderboards guide in the docs covers everything not specific to a single use case.
And if the build-vs-buy question is still open on your team, the Trophy buy vs. build page has the full cost and timeline breakdown for leaderboards alongside the other gamification features.
Get the latest on gamification
Product updates, best practices, and insights on retention and engagement — delivered straight to your inbox.