Skip to main content
This guide walks you through creating a working leaderboard with Convex Aggregate. You’ll learn how to configure aggregates, write data, and query aggregated results.
This quickstart assumes you’ve already installed the Aggregate component.

Define Your Schema

First, create a table to store your data. For this example, we’ll track game scores:
convex/schema.ts
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";

export default defineSchema({
  leaderboard: defineTable({
    name: v.string(),
    score: v.number(),
  }),
});

Configure Your Aggregate

Create a TableAggregate to track scores. We’ll create two different views:
  1. Sort by score (for global rankings)
  2. Group by user and score (for user statistics)
convex/leaderboard.ts
import { TableAggregate } from "@convex-dev/aggregate";
import { components } from "./_generated/api";
import type { DataModel } from "./_generated/dataModel";

// Aggregate sorted by score (highest first)
const aggregateByScore = new TableAggregate<{
  Key: number;
  DataModel: DataModel;
  TableName: "leaderboard";
}>(components.aggregateByScore, {
  sortKey: (doc) => -doc.score, // Negative for descending order
});

// Aggregate grouped by user, then score
const aggregateScoreByUser = new TableAggregate<{
  Key: [string, number];
  DataModel: DataModel;
  TableName: "leaderboard";
}>(components.aggregateScoreByUser, {
  sortKey: (doc) => [doc.name, doc.score],
  sumValue: (doc) => doc.score, // Track sum of scores
});

Set Up Automatic Syncing

Use triggers to keep aggregates in sync automatically:
convex/leaderboard.ts
import { Triggers } from "convex-helpers/server/triggers";
import { customCtx, customMutation } from "convex-helpers/server/customFunctions";
import { mutation as rawMutation } from "./_generated/server";

const triggers = new Triggers<DataModel>();
triggers.register("leaderboard", aggregateByScore.trigger());
triggers.register("leaderboard", aggregateScoreByUser.trigger());

// Custom mutation that automatically updates aggregates
export const mutation = customMutation(rawMutation, customCtx(triggers.wrapDB));

Write Data

Now you can insert scores, and the aggregates will update automatically:
convex/leaderboard.ts
import { v } from "convex/values";

export const addScore = mutation({
  args: {
    name: v.string(),
    score: v.number(),
  },
  handler: async (ctx, args) => {
    // This automatically updates both aggregates via triggers
    const id = await ctx.db.insert("leaderboard", {
      name: args.name,
      score: args.score,
    });
    return id;
  },
});

Query Aggregated Data

Now you can efficiently query your data in O(log n) time:
export const countScores = query({
  handler: async (ctx) => {
    return await aggregateByScore.count(ctx);
  },
});

User-Specific Statistics

Use the second aggregate to efficiently compute per-user statistics:
export const userAverageScore = query({
  args: { name: v.string() },
  handler: async (ctx, { name }) => {
    const count = await aggregateScoreByUser.count(ctx, {
      bounds: { prefix: [name] },
    });
    
    if (count === 0) return null;
    
    const sum = await aggregateScoreByUser.sum(ctx, {
      bounds: { prefix: [name] },
    });
    
    return sum / count;
  },
});

export const userHighScore = query({
  args: { name: v.string() },
  handler: async (ctx, { name }) => {
    const item = await aggregateScoreByUser.max(ctx, {
      bounds: { prefix: [name] },
    });
    
    return item?.sumValue ?? null;
  },
});

Try It Out

Add some test data from the Convex Dashboard:
// Run these from the Dashboard
await addScore({ name: "Alice", score: 1000 });
await addScore({ name: "Bob", score: 1500 });
await addScore({ name: "Alice", score: 1200 });

// Then query:
await countScores(); // Returns 3
await userAverageScore({ name: "Alice" }); // Returns 1100
await userHighScore({ name: "Bob" }); // Returns 1500
await rankOfScore({ score: 1200 }); // Returns Alice's rank

Backfilling Existing Data

If you’re adding aggregates to an existing table with data, see the Migrations and Backfills guide for instructions on backfilling existing records.

Next Steps

Core Concepts

Learn about keys, sorting, grouping, and namespacing

Use Cases

Explore real-world examples like leaderboards and pagination

API Reference

Browse the complete API documentation

Performance

Optimize for high-throughput workloads

Build docs developers (and LLMs) love