Skip to main content

Global Rankings

The CodeJam leaderboard ranks all players by total XP, creating a competitive environment where skill and consistency determine your position.

Ranking System

Your global rank is calculated dynamically based on XP:
convex/users.ts
export const getRank = query({
  args: {},
  handler: async (ctx) => {
    const userId = await getAuthUserId(ctx);
    if (!userId) return 0;

    const user = await ctx.db.get(userId);
    if (!user || user.xp === undefined) return 0;

    const betterUsers = await ctx.db
      .query("users")
      .withIndex("by_xp", q => q.gt("xp", user.xp!))
      .filter(q => q.neq(q.field("isAnonymous"), true))
      .collect();
    
    return betterUsers.length + 1;
  }
});
Your rank = (number of users with more XP than you) + 1. Anonymous users are excluded from rankings.

XP Index

Efficient ranking relies on database indexing:
convex/schema.ts
users: defineTable({
  // ... fields
  xp: v.optional(v.number()),
  level: v.optional(v.number()),
}).index("by_xp", ["xp"])
The by_xp index enables fast queries for:
  • Global rankings
  • Top player lists
  • Percentile calculations

Top Players

View the highest-ranking players on the leaderboard.

Top Users Query

Retrieve the top players:
convex/users.ts
export const getTopUsers = query({
  args: { limit: v.optional(v.number()) },
  handler: async (ctx, args) => {
    const limit = args.limit || 50;
    const users = await ctx.db
      .query("users")
      .withIndex("by_xp")
      .order("desc")
      .filter(q => q.neq(q.field("isAnonymous"), true))
      .take(limit);

    return users.map(u => ({
      _id: u._id,
      name: u.name,
      image: u.image,
      customAvatar: u.customAvatar,
      xp: u.xp || 0,
      level: u.level || 1,
    }));
  }
});

Leaderboard Display

The leaderboard shows:

Rank

Position based on XP

Player Name

Username or display name

Avatar

Profile picture or custom avatar

Total XP

Cumulative experience points

Level

Current progression level

Badges

Achievement highlights
The default leaderboard shows the top 50 players. Pass a custom limit to retrieve more or fewer.

Game-Specific Stats

Beyond global rankings, track per-challenge performance.

Game Stats Schema

Each game tracks individual player stats:
convex/schema.ts
game_stats: defineTable({
  userId: v.id("users"),
  gameId: v.string(), // 'syntax-smasher', 'css-combat', etc.
  bestScore: v.number(),
  gamesPlayed: v.number(),
  lastPlayed: v.number(),
}).index("by_user_game", ["userId", "gameId"])
  .index("by_game", ["gameId"])

Challenge Leaderboards

Each challenge can have its own leaderboard:
gameId: "syntax-smasher"

Top scores:
1. Player1 - 5000 pts
2. Player2 - 4850 pts
3. Player3 - 4720 pts

Best Scores

Only your highest score per challenge is saved:
convex/social.ts
await ctx.db.patch(existing._id, {
  bestScore: Math.max(existing.bestScore, args.score),
  gamesPlayed: existing.gamesPlayed + 1,
  lastPlayed: Date.now()
});
Play challenges repeatedly to improve your bestScore and climb the challenge-specific leaderboard.

Player Statistics

Detailed stats provide insight into player performance:

Personal Stats

  • Total XP: Cumulative across all activities
  • Global Rank: Position among all players
  • Level: Calculated from total XP
  • Quests Completed: Total activities finished
  • Best Score: Highest score per challenge
  • Games Played: Attempts per challenge
  • Last Played: Timestamp of most recent attempt
  • Challenge Rank: Position on challenge leaderboard
  • Current Streak: Consecutive active days
  • Weekly XP: Last 7 days of activity
  • Daily XP Average: Mean XP per active day
  • Peak Performance: Highest single-day XP

Anonymous Users

Anonymous users are excluded from competitive features:
Filter
filter(q => q.neq(q.field("isAnonymous"), true))
Anonymous users cannot:
  • Appear on leaderboards
  • Compete in rankings
  • Challenge other players
  • Unlock social achievements
Sign up to access competitive features!

Ranking Updates

Rankings update in real-time as players earn XP.

Update Flow

1

Player Earns XP

Complete a challenge or quest
2

Activity Logged

logActivity mutation updates user XP
3

Rank Recalculated

Next getRank query reflects new XP
4

Leaderboard Refreshes

getTopUsers query returns updated rankings
Rankings are dynamically calculated - no manual refresh needed. Queries always return current data.

Leaderboard Competition

Multiple competitive dimensions:

XP Race

Climb the global leaderboard by earning more XP than other players

Challenge Mastery

Achieve the highest score on individual challenges

Consistency Battle

Maintain the longest active streak

Speed Runs

Complete time-based objectives faster than competitors

Battle System Integration

Leaderboards power the battle system:

Ghost Battles

Challenge players by competing against their best scores:
convex/social.ts
export const getGhostScore = query({
  args: { userId: v.id("users"), gameId: v.string() },
  handler: async (ctx, args) => {
    const stats = await ctx.db
      .query("game_stats")
      .withIndex("by_user_game", (q) => 
        q.eq("userId", args.userId).eq("gameId", args.gameId)
      )
      .first();
    
    return stats ? stats.bestScore : 0;
  },
});
Ghost battles use the opponent’s bestScore as the target to beat.

Battle Outcomes

Battle results affect stats:
convex/social.ts
const isWin = args.score > (battle.opponentScore || 0);

await ctx.db.patch(args.battleId, {
  challengerScore: args.score,
  status: "completed",
  winnerId: isWin ? userId : battle.opponentId
});
Winning ghost battles against top players proves your skill and boosts your reputation!
Find specific players to challenge:
convex/users.ts
export const searchUsers = query({
  args: { query: v.string() },
  handler: async (ctx, args) => {
    const userId = await getAuthUserId(ctx);
    if (!userId) return [];
    
    if (!args.query || args.query.length < 2) return [];

    const users = await ctx.db
      .query("users")
      .filter(q => q.neq(q.field("isAnonymous"), true))
      .collect();

    return users.filter(u => 
      (u.name?.toLowerCase().includes(args.query.toLowerCase()) || 
       u.email?.toLowerCase().includes(args.query.toLowerCase())) &&
       u._id !== userId // Exclude self
    ).slice(0, 5).map(u => ({
      _id: u._id,
      name: u.name,
      email: u.email,
      image: u.image,
      customAvatar: u.customAvatar,
      level: u.level
    }));
  }
});
Search supports:
  • Name matching (case-insensitive)
  • Email matching
  • Returns top 5 results
  • Excludes yourself and anonymous users

Leaderboard Strategy

1

Focus on High-XP Challenges

Expert challenges (300 XP) provide the fastest rank gains
2

Maintain Consistency

Daily play earns steady XP and protects your streak
3

Perfect Your Scores

Replay challenges to improve your best scores
4

Complete All Objectives

Full objective completion maximizes XP per challenge
5

Challenge Top Players

Ghost battles against leaders test your skills

Live Activity Feed

See real-time global activity:
convex/activity.ts
export const getSidebarFeed = query({
  args: {},
  handler: async (ctx) => {
    const recentActivity = await ctx.db
      .query("activity_logs")
      .order("desc")
      .take(1);
    
    let activityItem = null;
    if (recentActivity.length > 0) {
      const act = recentActivity[0];
      const user = await ctx.db.get(act.userId);
      if (user) {
        activityItem = {
          type: "event",
          badge: "Live",
          badgeColor: "bg-nav-orange",
          title: user.name || "Anonymous",
          subtitle: `${act.type} • +${act.xp} XP`
        };
      }
    }
    return [activityItem];
  },
});
The activity feed shows the most recent XP-earning action platform-wide, creating a sense of live competition.

Next Steps

Progression

Learn how to earn XP and level up

Challenges

Explore challenges to climb rankings

Achievements

Unlock badges to showcase mastery

Battle System

Challenge players in ghost battles

Build docs developers (and LLMs) love