Leaderboards in headless gamification
A leaderboard is a named ranking of participants by a chosen metric over a period. Bricqs ships the engine that takes your points (or any other ranked metric), buckets it into periods, and returns the top N plus individual rank lookups. This page covers what a leaderboard system actually is, how Bricqs computes rankings, and the patterns you will reach for when wiring leaderboards into a live UI.
- A leaderboard is a sorted list of participants ranked by a chosen metric, scoped to a time period; rankings are pure projections with no writes and no facts emitted.
- Top-N reads hit a per-definition in-memory cache (30s default); individual rank lookups skip the cache and are always recomputed live.
- Use period_key as a stable cache key in your own SWR or CDN layer to get free invalidation at period rollover without listening for events.
- Always pair an all-time leaderboard with at least one resettable period so new users see a credible path to the top each cycle.
- When prizes are real money, use the contests system rather than a raw leaderboard snapshot; contests provide idempotent prize allocation, anti-fraud, and dispute handling.
What a leaderboard is, in any gamification system
Before the API. Every leaderboard system, regardless of vendor, decomposes into the same primitives. If you understand these, the Bricqs implementation is just one mapping you could make.
A leaderboard is a sorted list of participants ranked by a chosen metric, scoped to a time period. The metric is a number that can be compared (points balance, lifetime points, event count). The period defines what window the ranking covers (the current week, the current month, all-time). The output is a top-N list and a per-participant rank lookup.
Engineers wiring leaderboards into product surfaces (community pages, contest screens, profile widgets); growth and lifecycle teams choosing what to rank and how often to reset. Leaderboards are owned by product, computed by the platform, surfaced by engineering.
Points are the meter (or one of many possible metrics). A leaderboard is the projection that turns the meter into a competitive ranking. You can run a points program without a leaderboard, but a leaderboard always needs a metric to sort by.
A contest is a time-bound competition with a defined start and end, prizes, and entry rules. A leaderboard is a ranking primitive. Contests almost always use a leaderboard internally; not every leaderboard is a contest.
Tiers are absolute status levels (everyone who hits 10k lifetime points is Gold). A leaderboard is relative (you are rank 5 because four people have more than you right now). Tiers are sticky; leaderboard rank changes by the hour.
A milestone is an absolute threshold ("hit 1000 referrals"). A leaderboard is a comparison across participants. Milestones reward consistency; leaderboards reward outperforming peers.
The decomposition every leaderboard system uses: a definition (the leaderboard as a noun: code, name, metric, default period, segment) and a view (the leaderboard as a verb: the top-N entries plus optional individual-rank queries). Definitions are tenant-scoped configuration. Views are read-only projections computed on demand.
The two operations every API supports: list the top-N entries (optionally filtered by period), and look up a single participant’s rank. Everything else is convenience: change tracking, percentile, segmented sub-leaderboards.
The hardest design decision is not the schema, it is the freshness contract. Two patterns dominate. Pre-built leaderboard table: maintain a separate ranked table (often a Redis sorted set), update it on every relevant event. Fast reads, expensive writes, easy to drift if a write is missed. Computed on read: query the source data on every read, cache for a short TTL. Slower per request, no drift, scales linearly with the number of viewers. Bricqs uses the second pattern with a 30-second default cache; the Redis sorted-set version is on the roadmap for the largest tenants.
When leaderboards are the right tool, and when they are not
Leaderboards are powerful for competitive moments and dangerous when used as a permanent retention layer. Pick the moments where competition motivates the audience you actually have.
You have a time-bound contest with prizes
Contests need a ranking surface so users see where they stand. Weekly or monthly contests with a top-3 or top-10 prize map cleanly onto a leaderboard with a matching period.
The audience is competitive by self-selection
Gaming, fitness, sales, esports communities. The leaderboard lights up engagement because the audience already cares about ranking. Most consumer audiences do not; check before assuming.
You can scope the leaderboard tightly enough to feel reachable
Global all-time leaderboards reward incumbents and discourage new users. Scope by week, by segment (city, team, cohort), or by friend graph so a new participant can see a credible path to the top.
You need a low-cost relative-rank widget
Even when the leaderboard is not the main surface, a small “you are rank 14 of 1250” chip on a profile can drive return visits without dominating the UI.
The audience is not naturally competitive
Putting a leaderboard in front of a non-competitive audience makes the bottom 80% feel bad. Mark a few users as visible winners; leave the rest in private progress views.
The metric can be gamed
If users can inflate the metric (refresh-spam, multi-account, bot-driven scoring), the leaderboard becomes a list of cheaters within a week. Either harden the metric (server-side scoring, anti-fraud) or pick a different surface.
There is no period reset
Pure all-time leaderboards are dominated by users who started early. New participants check once and never look again. Always offer at least one resettable period (weekly, monthly) alongside any all-time view.
The prize is real money and rank is the only proof of qualification
Leaderboards are read-side projections; they reflect the underlying data with a short delay. Use the contest system, not a raw leaderboard, when prizes are real money. Contests have anti-fraud, dispute handling, and prize-allocation logic the raw leaderboard does not.
It is a casual one-time activity
A one-day quiz with a leaderboard surface adds noise. The user finishes the quiz, looks at their rank, never comes back. Surface a personal score and call out the top 3, but skip the full table.
- Looking for the marketing-team take? The strategy guide on leaderboards covers when ranking surfaces fit a program, fairness design, KPIs, and example flows.
- Configure in the dashboard: Progression → Leaderboards → New Leaderboard.
Bricqs models leaderboards with the standard two-part decomposition above: a definition (the leaderboard rules) and a view (the live ranking). You create the definition through the dashboard or the admin API. Views are computed at read time from the underlying data, so changes to scores show up at the next read (subject to a short TTL cache).
Bricqs handles the heavy parts for you. Rankings run as PostgreSQL queries against the points and scoring data; the top-N response is cached in memory for 30 seconds by default (configurable per definition). Individual rank lookups are always live. No background worker, no Redis cluster, no sorted-set maintenance.
What a leaderboard looks like
Two shapes you will see in API responses. The first is the top-N response. The second is the single-participant rank lookup.
{
"leaderboard_code": "top_scorers",
"period": "monthly",
"period_key": "2026-05", // bucket key, useful for cache busting
"entries": [
{ "rank": 1, "participant_id": "user_456", "display_name": "Jordan", "score": 5000, "change": "up" },
{ "rank": 2, "participant_id": "user_123", "display_name": "Riley", "score": 4500, "change": "same" },
{ "rank": 3, "participant_id": "user_789", "display_name": "Avery", "score": 4200, "change": "new" }
],
"total_participants": 1250
}Period keys make caches safe. The period_key field changes when the period rolls over (e.g. monthly key 2026-05 to 2026-06). Use it as the cache key in your own SWR or CDN layer to get free invalidation on rollover without listening for events.
The change field is best-effort. It compares the participant’s rank in the current view to their rank in the previous snapshot. Useful for arrow indicators (up / down / equal / new) but do not build business logic on top of it; the resolution depends on cache TTL.
{
"participant_id": "user_123",
"rank": 2,
"score": 4500,
"total_participants": 1250,
"percentile": 99.8
}Percentile is computed live. percentile = round((1 - rank/total) * 100, 1). Render it as “Top 0.2%” for the highest-ranked users instead of just “rank 2”; it reads better at small numbers and at very large totals.
The rank field is always live. Unlike the top-N response, this endpoint skips the cache so it reflects the most recent activity. Use it for the participant’s own “you are rank N” chip where freshness matters most.
What happens when you request a leaderboard
One sequence covers every read. Leaderboards are pure projections: no facts emitted, no background workers, no writes.
- 01
Your client requests the leaderboard
Either a top-N call (GET /leaderboards/{code}) or an individual rank lookup (GET /leaderboards/{code}/rank/{pid}). - 02
The engine resolves the definition
Looks up the leaderboard by code. Returns 404 if the definition does not exist or is inactive. Reads the metric type, the default period, and the cache TTL from the definition. - 03
For top-N reads, check the in-memory cache
If a cached response for(code, period, limit)exists and is fresh (default 30 seconds), return it immediately. For individual rank reads, the cache is skipped: rank is always recomputed live so it reflects the most recent activity. - 04
Otherwise, run the ranking query
A PostgreSQL query joins the points or scoring data with the participant table, filtered by period, sorted by the metric. Returns the top-N entries or the single participant's rank. - 05
Cache the top-N response
Store the result in the in-memory cache keyed by(code, period, limit)with a TTL equal to the definition’s configured cache window. - 06
The SDK refreshes on a poll interval
TheuseLeaderboardhook polls every 30 seconds by default. Combined with the server-side cache, this means several thousand viewers can watch a live leaderboard for the cost of one query per 30 seconds.
Read leaderboards from any backend
Two endpoints cover almost every case. All endpoints are under /api/v1/gamify/ and authenticate with an API key (bq_live_ for production, bq_test_ for sandbox).
Top-N ranking
Returns the top entries for a leaderboard, optionally filtered by period. Cached for 30 seconds by default (configurable per definition).
curl 'https://api.bricqs.com/api/v1/gamify/leaderboards/top_scorers?limit=10&period=monthly' \
-H "Authorization: Bearer bq_live_xxxxx"
# Query params:
# limit=N number of entries to return (default 20, max 100)
# period=monthly override the definition's default (all_time|yearly|monthly|weekly|daily)
# Response (200 OK)
{
"leaderboard_code": "top_scorers",
"period": "monthly",
"period_key": "2026-05",
"entries": [
{ "rank": 1, "participant_id": "user_456", "display_name": "Jordan", "score": 5000, "change": "up" },
{ "rank": 2, "participant_id": "user_123", "display_name": "Riley", "score": 4500, "change": "same" },
{ "rank": 3, "participant_id": "user_789", "display_name": "Avery", "score": 4200, "change": "new" }
],
"total_participants": 1250
}Limit caps at 100. If you need a longer tail, paginate by changing the period scope or by rendering a “view full ranking” modal that fetches additional pages on demand.
period_key is stable for the window. Use it as a cache key in your own SWR or CDN layer; it changes when the period rolls over, giving you free invalidation without listening for events.
Individual rank lookup
Returns one participant’s rank, score, total participants, and percentile. Always live; not cached. Use this for the “you are rank N” chip on a participant’s own surface.
curl https://api.bricqs.com/api/v1/gamify/leaderboards/top_scorers/rank/user_123 \
-H "Authorization: Bearer bq_live_xxxxx"
# Response (200 OK)
{
"participant_id": "user_123",
"rank": 2,
"score": 4500,
"total_participants": 1250,
"percentile": 99.8
}Manage leaderboard definitions (admin only)
Most teams create and edit leaderboards in the dashboard. If you want to script it (e.g. seed leaderboards in CI), use the admin endpoints. They require an admin-scoped API key.
GET /api/v1/gamify/leaderboards
GET /api/v1/gamify/leaderboards/{leaderboard_code}
POST /api/v1/gamify/leaderboards
{ "code": "top_scorers", "name": "Top Scorers", "metric": "points_balance",
"default_period": "monthly", "cache_ttl_seconds": 30 }
PATCH /api/v1/gamify/leaderboards/{leaderboard_code}
{ "cache_ttl_seconds": 60 }
DELETE /api/v1/gamify/leaderboards/{leaderboard_code}Render a leaderboard with one hook
The useLeaderboard hook returns top entries, the current participant's rank, and total participants in one call. It polls on a configurable interval; pair with the period_key for cache-safe rendering.
"use client";
import { useLeaderboard } from "@bricqs/sdk-react";
export function LeaderboardPanel({
engagementId,
code,
}: {
engagementId: string;
code: string;
}) {
const {
entries, // top-N entries with rank, name, score, change
myRank, // the current participant's rank (or null)
totalParticipants, // overall participant count for the period
isLoading,
error,
refresh, // call refresh() to manually re-fetch
} = useLeaderboard({ engagementId, code, limit: 10, refreshInterval: 30000 });
if (isLoading) return <p>Loading leaderboard...</p>;
if (error) return <p>Could not load leaderboard.</p>;
return (
<div>
{myRank && (
<p className="text-sm text-slate-600 mb-3">
You are rank {myRank.rank} of {totalParticipants} ({myRank.percentile}%)
</p>
)}
<ol className="space-y-1">
{entries.map((e) => (
<li
key={e.participant_id}
className="flex items-baseline justify-between rounded-lg px-3 py-2 border border-slate-200"
>
<span className="font-mono text-sm w-6">{e.rank}</span>
<span className="flex-1 ml-3 font-medium">{e.display_name}</span>
<span className="font-semibold tabular-nums">{e.score.toLocaleString()}</span>
{e.change === "up" && <span className="ml-2 text-emerald-600">up</span>}
{e.change === "down" && <span className="ml-2 text-rose-600">down</span>}
{e.change === "new" && <span className="ml-2 text-xs text-slate-500">NEW</span>}
</li>
))}
</ol>
</div>
);
}Default poll interval is 30 seconds. Pass refreshInterval: 0 to disable polling. The server-side cache (30 seconds default) means the SDK’s 30-second poll mostly hits warm cache, so even busy leaderboard surfaces are cheap.
Use code for progression leaderboards, challengeId for challenge leaderboards. The hook routes to the right endpoint based on which prop you pass; the return shape is the same.
Field shape. The hook returns entries with snake_case fields (participant_id, display_name) to match the wire format, plus top-level camelCase shortcuts (myRank, totalParticipants). A consistent camelCase boundary at the SDK is on the roadmap.
Recipes you will reach for
Four patterns that cover most leaderboard implementations in production. Each is a small composition of the primitives above.
Pattern 1: Server-rendered podium, client-rendered tail
The top 3 are the visual hook and should appear instantly on first load. The remaining entries can stream in client-side with the polling hook. Server-render the podium from the REST endpoint; mount the SDK hook below it.
import { LeaderboardPanel } from "@/components/LeaderboardPanel";
async function loadPodium(code: string) {
const res = await fetch(
`https://api.bricqs.com/api/v1/gamify/leaderboards/${code}?limit=3`,
{
headers: { Authorization: `Bearer ${process.env.BRICQS_API_KEY}` },
next: { revalidate: 30 },
}
);
return res.json();
}
export default async function LeaderboardPage() {
const podium = await loadPodium("top_scorers");
return (
<main>
<section className="podium">
{podium.entries.map((e: any) => (
<article key={e.participant_id}>
<h3>#{e.rank} {e.display_name}</h3>
<p>{e.score.toLocaleString()} points</p>
</article>
))}
</section>
<LeaderboardPanel engagementId={process.env.NEXT_PUBLIC_ENGAGEMENT_ID!} code="top_scorers" />
</main>
);
}Use next: { revalidate: 30 } so Next.js's ISR cache matches the server-side leaderboard cache TTL. The podium re-renders every 30 seconds without per-request load on the API.
Pattern 2: “You are rank N” chip on a profile
Sometimes you do not want a full leaderboard surface, just a hint of competitive context. The individual rank endpoint is always live (no cache) and cheap to call. Render it as a compact chip alongside the user's points balance.
"use client";
import { useEffect, useState } from "react";
export function MyRankChip({
code,
participantId,
}: {
code: string;
participantId: string;
}) {
const [rank, setRank] = useState<{ rank: number; percentile: number } | null>(null);
useEffect(() => {
async function load() {
const res = await fetch(
`/api/bricqs/leaderboards/${code}/rank/${participantId}`
// server-side proxy so the API key stays off the client
);
setRank(await res.json());
}
load();
}, [code, participantId]);
if (!rank) return null;
return (
<span className="inline-flex items-center gap-1 rounded-full bg-amber-50 px-2 py-0.5 text-xs">
Rank #{rank.rank} (top {(100 - rank.percentile).toFixed(1)}%)
</span>
);
}Always proxy the API call through your server so the API key stays off the client. Cache the response in your own SWR or React Query layer if you render the chip on many pages.
Pattern 3: Highlight the period rollover
When a period rolls over (weekly, monthly), users expect their rank to reset. Watch the period_key field to detect the change client-side and show a fresh-start banner.
"use client";
import { useEffect, useRef } from "react";
import { useLeaderboard } from "@bricqs/sdk-react";
export function PeriodRolloverBanner({ engagementId, code }: { engagementId: string; code: string }) {
const { entries } = useLeaderboard({ engagementId, code });
const lastSeenKey = useRef<string | null>(null);
useEffect(() => {
// The hook surfaces period_key on each entry; pick it from the first
const currentKey = (entries[0] as any)?.period_key ?? null;
if (currentKey && lastSeenKey.current && currentKey !== lastSeenKey.current) {
// Period rolled over; show a one-time banner
showToast("A new week starts now. Your rank resets.");
}
lastSeenKey.current = currentKey;
}, [entries]);
return null;
}You can also compute the rollover server-side and surface a dedicated banner during the first 24 hours of a new period; this is what most production apps do for the cleanest UX.
Pattern 4: Segment-scoped leaderboard for fairness
Global leaderboards reward incumbents. Scope by cohort (signup month), region (city, country), or team (friend graph) so a new participant can see a credible path to a meaningful top spot. Create a separate leaderboard definition per segment; the dashboard supports segment filters on the definition.
await fetch("https://api.bricqs.com/api/v1/gamify/leaderboards", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.BRICQS_ADMIN_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
code: "top_scorers_may_2026_cohort",
name: "Top Scorers (May 2026 cohort)",
metric: "points_balance",
default_period: "monthly",
segment: { signup_month: "2026-05" }, // only participants who signed up in May
}),
});Render the cohort-scoped leaderboard alongside the global one. New users see themselves in a fair fight; competitive users see the global ranks they care about. Most successful consumer programs run both at once.
Design choices teams regret later
Each pitfall is a structural decision you make at design time. Catch them in the dashboard and the spec, not after the program is live.
Picking all-time as the only period. New users see they have no chance against incumbents, check the leaderboard once, and never look again. The ranking becomes a graveyard of users who joined first.
Design choice: always pair an all-time leaderboard with at least one resettable period (weekly is best for consumer apps). Surface the participant's rank in both. The resettable period gives new users a credible top spot every cycle; the all-time view rewards long-term commitment.
Running a single global leaderboard for an audience that is not uniformly competitive. The top 1% feel great; the bottom 80% feel left out and either leave or learn to ignore the surface.
Design choice: scope the leaderboard so most participants can see themselves credibly in it. Cohort by signup month, segment by city or team, or scope by friend graph. The dashboard supports per-definition segments; create a fair-fight leaderboard alongside the global one.
Choosing a metric that participants can inflate (refresh-spam, multi-account, bot-driven scoring). Within a week the leaderboard is a list of cheaters and trust in the surface collapses.
Design choice: pick a metric the server controls end-to-end (server-side score, verified events, lifetime points from facts you trust). If the metric must come from client actions, add anti-fraud at the metric layer before the leaderboard reads it.
Using a raw leaderboard snapshot as the source of truth for prize allocation in a money contest. Cache TTL plus clock skew means two observers at midnight see different top 3, leading to disputes you cannot resolve.
Design choice: when prizes are real money, use the dedicated contests system, not the leaderboard. Contests have prize allocation against authoritative scoring data with idempotency, anti-fraud, and dispute handling. The leaderboard is the live view; the contest is the source of truth for prizes.
Shipping a leaderboard without naming who would not enjoy seeing it. Decorative leaderboards on non-competitive audiences silently depress engagement: the bottom of the table is most of your users.
Design choice: write down the audience and the moment before approving the surface. If you cannot name a competitive audience or a contest hook that earns the leaderboard, replace it with a personal-progress widget (a chip, a milestone bar) and surface ranking only inside opt-in contest moments.
Where to go next
Concepts, references, and patterns this page anchors. Pick the next read based on what you are about to build.
Progression overview
How leaderboards, points, tiers, badges, and streaks fit together.
Read itPoints
Most leaderboards rank by points (balance or lifetime). Covers the ledger, idempotency, and the difference between current and lifetime totals.
Read itTier
Tiers are absolute status; leaderboards are relative rank. Pair them: the leaderboard rewards in-period competition, the tier rewards lifetime achievement.
Read itLeaderboards REST API
Full endpoint detail: every parameter, every response field, every status code.
Read itProgression hooks
useLeaderboard reference + the other progression hooks (useTier, usePoints, useBadgesHeadless).
Read itWeekly contest
End-to-end walkthrough of a time-bound contest that uses a leaderboard for the live view and the contests system for the authoritative prize allocation.
Read itCommon questions when integrating
Build with leaderboards
Pair this with the strategy guide on leaderboard systems, or jump straight to the SDK setup page.
