Time Periods
Daily, weekly, monthly, or all-time rankings
Automatic Resets
Scores reset automatically at period boundaries
Real-time Updates
Live rank updates as scores change
Privacy Controls
Users can opt out of public leaderboards
How Leaderboards Work
Leaderboards rank users by a numeric score. Scores can be submitted from any action in your app. The platform handles ranking, ties, and period resets automatically.
Configuration
Define leaderboards in your engagement config. Each leaderboard has its own period and scoring rules:
import { defineEngagement, leaderboard } from '@sylphx/sdk/engagement'
export const engagement = defineEngagement({
leaderboards: {
// Weekly competition - resets every Monday
weekly_points: leaderboard({
name: 'Weekly Points',
description: 'Top point earners this week',
period: 'weekly',
sortOrder: 'desc', // Highest score wins
maxEntries: 100, // Track top 100
tieBreaker: 'earliest', // First to reach score wins ties
}),
// Monthly challenge
monthly_challenges: leaderboard({
name: 'Monthly Challenges',
description: 'Most challenges completed this month',
period: 'monthly',
sortOrder: 'desc',
maxEntries: 50,
}),
// All-time high scores
all_time_score: leaderboard({
name: 'Hall of Fame',
description: 'Highest scores ever achieved',
period: 'alltime',
sortOrder: 'desc',
maxEntries: 1000,
}),
// Daily speedrun - lowest time wins
daily_speedrun: leaderboard({
name: 'Daily Speedrun',
description: 'Fastest completion times today',
period: 'daily',
sortOrder: 'asc', // Lowest score (time) wins
maxEntries: 100,
}),
},
})| Property | Type | Description |
|---|---|---|
namerequired | string | Display name for the leaderboard |
description | string | User-facing description of how to earn points |
periodrequired | 'daily' | 'weekly' | 'monthly' | 'alltime' | How often the leaderboard resets (alltime = never) |
sortOrder | 'asc' | 'desc' | Sort direction: 'desc' for highest-wins, 'asc' for lowest-wins (default: desc) |
maxEntries | number | Maximum number of entries to track (default: 1000) |
tieBreaker | 'earliest' | 'latest' | 'alphabetical' | How to break ties (default: earliest) |
aggregation | 'sum' | 'max' | 'latest' | How multiple submissions combine: 'sum' adds them, 'max' keeps highest, 'latest' replaces (default: sum) |
Submitting Scores
Submit scores from client or server. Multiple submissions aggregate based on your config.
'use client'
import { useLeaderboard } from '@sylphx/sdk/react'
export function PointsDisplay() {
const {
entries, // Top entries on leaderboard
userRank, // Current user's rank info
submitScore, // Function to submit score
isLoading,
} = useLeaderboard('weekly_points')
const handleTaskComplete = async (points: number) => {
// Submit points for completing a task
const result = await submitScore(points, {
metadata: { source: 'task_complete' }
})
// result: {
// newScore: 450,
// previousRank: 8,
// newRank: 5,
// percentile: 92
// }
if (result.newRank < result.previousRank) {
toast.success(`You moved up to #${result.newRank}!`)
}
}
return (
<div>
<div className="text-lg font-bold mb-4">
Your Rank: #{userRank?.rank ?? '-'}
({userRank?.score?.toLocaleString() ?? 0} pts)
</div>
<button
onClick={() => handleTaskComplete(50)}
className="btn btn-primary"
>
Complete Task (+50 pts)
</button>
</div>
)
}Score Aggregation
sum for cumulative leaderboards (total points), max for high-score boards (best single performance), and latest for current-state rankings.Fetching Leaderboards
Retrieve leaderboard data with pagination and filtering options:
'use client'
import { useLeaderboard } from '@sylphx/sdk/react'
export function WeeklyLeaderboard() {
const {
entries, // LeaderboardEntry[]
userRank, // User's own rank info
isLoading,
loadMore, // Load next page
hasMore, // More entries available
} = useLeaderboard('weekly_points', {
limit: 20, // Entries per page
})
return (
<div className="space-y-2">
{entries?.map((entry, i) => (
<div
key={entry.userId}
className={`flex items-center p-3 rounded-lg ${
entry.isCurrentUser ? 'bg-primary/10 border border-primary' : 'bg-muted/50'
}`}
>
<span className="w-12 text-center font-bold">
{entry.rank === 1 ? '🥇' :
entry.rank === 2 ? '🥈' :
entry.rank === 3 ? '🥉' : `#${entry.rank}`}
</span>
<div className="flex-1">
<div className="font-medium">{entry.displayName}</div>
{entry.isCurrentUser && (
<div className="text-xs text-primary">You</div>
)}
</div>
<span className="font-mono">{entry.score.toLocaleString()}</span>
</div>
))}
{hasMore && (
<button onClick={loadMore} className="btn btn-ghost w-full">
Load More
</button>
)}
</div>
)
}Time Periods
Leaderboards with periods reset automatically at the start of each period:
Resets at midnight UTC. Great for daily challenges and speedruns.
Resets Monday 00:00 UTC. Most common for point-based competitions.
Resets 1st of month 00:00 UTC. Good for longer competitions.
Never resets. Use for hall of fame or permanent records.
// Access historical leaderboard data
const archives = await platform.engagement.getLeaderboardArchives('weekly_points', {
limit: 10, // Last 10 periods
})
// archives: [
// { periodStart: '2024-01-15', periodEnd: '2024-01-22', winner: {...}, topEntries: [...] },
// { periodStart: '2024-01-08', periodEnd: '2024-01-15', winner: {...}, topEntries: [...] },
// ...
// ]
// Get a specific past period
const lastWeek = await platform.engagement.getLeaderboardArchive('weekly_points', {
periodStart: '2024-01-08',
})Rewards & Prizes
Automatically distribute rewards when periods end:
export const engagement = defineEngagement({
leaderboards: {
weekly_points: leaderboard({
name: 'Weekly Competition',
period: 'weekly',
sortOrder: 'desc',
// Define rewards for top placements
rewards: [
{ rank: 1, achievement: 'weekly_champion', points: 500 },
{ rank: 2, achievement: 'weekly_silver', points: 300 },
{ rank: 3, achievement: 'weekly_bronze', points: 200 },
{ rankRange: [4, 10], points: 100 },
{ rankRange: [11, 50], points: 50 },
],
// Webhook called when period ends
onPeriodEnd: 'https://your-app.com/api/webhooks/leaderboard-end',
}),
},
})
// In your webhook handler:
export async function POST(request: Request) {
const { leaderboardId, period, winners } = await request.json()
// winners: [
// { userId: '...', rank: 1, score: 2450, reward: { achievement: 'weekly_champion', points: 500 } },
// { userId: '...', rank: 2, score: 2100, reward: { achievement: 'weekly_silver', points: 300 } },
// ...
// ]
for (const winner of winners) {
await sendEmail(winner.userId, {
subject: `🏆 You placed #${winner.rank} this week!`,
body: `Congratulations! You earned ${winner.reward.points} bonus points.`,
})
}
}Privacy & Opt-Out
Respect user privacy with opt-out options and anonymization:
// User privacy settings
await platform.engagement.setPrivacy(userId, {
showOnLeaderboards: false, // Don't show on public leaderboards
})
// User's scores are still tracked, but:
// - They don't appear in public leaderboard queries
// - They can still see their own rank privately
// - They can re-enable visibility anytime
// Fetch leaderboard with anonymization
const leaderboard = await platform.engagement.getLeaderboard('weekly_points', {
anonymize: true,
})
// Shows "Player #1", "Player #2" instead of real names
// In React, respect user preference
const { entries, userRank } = useLeaderboard('weekly_points')
// If current user has showOnLeaderboards: false,
// they see their rank but aren't in the public entries listDisplay Components
Pre-built components for common leaderboard UIs:
import {
LeaderboardTable, // Full table with ranks, names, scores
LeaderboardPodium, // Top 3 with medals
LeaderboardMini, // Compact view for sidebars
UserRankBadge, // Show current user's rank
} from '@sylphx/sdk/react'
// Full leaderboard table
<LeaderboardTable
leaderboardId="weekly_points"
limit={20}
highlightUser // Highlight current user's row
showPercentile // Show percentile for user
/>
// Podium for top 3
<LeaderboardPodium leaderboardId="weekly_points" />
// Compact mini view
<LeaderboardMini
leaderboardId="weekly_points"
entries={5}
/>
// User's rank badge
<UserRankBadge leaderboardId="weekly_points" />
// Renders: "#5 of 847 (Top 1%)"