Skip to main content

Leaderboards

Code First

Create competitive rankings that motivate users and drive engagement.

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.

Weekly Points Leaderboard
🥇Alice2,450
🥈Bob2,100
🥉Charlie1,890
#4Diana1,650
#5Eve1,520

Configuration

Define leaderboards in your engagement config. Each leaderboard has its own period and scoring rules:

engagement.config.ts
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,
    }),
  },
})
PropertyTypeDescription
namerequiredstringDisplay name for the leaderboard
descriptionstringUser-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)
maxEntriesnumberMaximum 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

Use 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:

Daily

Resets at midnight UTC. Great for daily challenges and speedruns.

Weekly

Resets Monday 00:00 UTC. Most common for point-based competitions.

Monthly

Resets 1st of month 00:00 UTC. Good for longer competitions.

All-Time

Never resets. Use for hall of fame or permanent records.

Period Archives
// 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:

engagement.config.ts
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 list

Display 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%)"