Leaderboard
Multi-metric rankings in isolated namespaces: submit scores for several metrics at once, query top-N by a weighted composite or a single metric, look up one player's rank and neighbors, filter by time window, and batch submissions. Not just for games - sales contests, GitHub stars, and poll results rank the same way.
When to reach for it: game season rankings, sales contests, engagement scores, any "who is winning" question over one or more numeric metrics.
When not to: bracket-based competitions (single/double elimination, swiss) belong to Tournament; event-driven reputation scoring (with its own reputation leaderboard) belongs to Reputation.
Concepts
- Namespace - an isolated leaderboard space (
season-1,daily-sales). Created on the first authorized submit and private to that creator by default. Readers and editors can be shared explicitly or through a workspace. - Metric - one scored dimension (
score,accuracy,revenue). A namespace holds several; each submit updates only the metrics it names. - Composite score - rankings combine a namespace's metrics into one number: the weighted sum of each entity's metric values, equal weights by default. Weights are supplied per query, not stored.
- Entity ID - who the score belongs to. Defaults to the JWT subject, so
two users submitting without
-inever collide; pass-ito submit on behalf of any ID. - Manifest - an optional metadata record (name, description, tags) for cataloging leaderboards. Manifests and namespaces are separate systems: creating a manifest also creates its owned namespace. Submitting scores still does not require a manifest. The access record exists immediately; namespace enumeration begins with the first score. Resetting scores preserves an existing manifest and its grants, while clearing its score registration.
Enterprise access
The namespace is the permission root for its manifest, metrics, entities and scores. A reader can query rankings and metadata; an editor can submit/delete scores and edit manifest metadata; a manager can share or delete the namespace. Default entity attribution is a convenience, not permission to write into another user's namespace. Cross-namespace aggregations require access to every source. Counts, enumeration and profile includes observe the same read boundary.
snug leaderboard create accepts --workspace-id and --visibility private|workspace.
The existing shared access API uses target leaderboards and the namespace as its
resource ID, including namespaces created without manifests. Groups, expiring grants
and service identities use the same grant machinery as other shared resources.
Submitting scores
snug leaderboard submit -n season-1 -m score:100,accuracy:80 -d '{"clan":"wolves"}'
snug leaderboard submit -n season-1 -i player-bob -m score:120,accuracy:40A submit replaces each named metric's value; it does not add to it.
Unnamed metrics survive - verified: after score:100,accuracy:80 a second
submit of score:50 left accuracy at 80 and the composite at 130. There is
no increment operation; to accumulate, read the current value and resubmit.
The response reports the entity's new standing:
{
"id": "player-bob",
"weighted_score": 160.0,
"rank": 2,
"total_entities": 2
}Batch submission
batch-submit posts a JSON array of {id, metrics, meta} objects in one
request - here every id is required, there is no JWT-subject default.
Entries are applied independently and the batch is capped (500 by default).
snug leaderboard batch-submit -n season-1 -f players.jsonRankings
snug leaderboard top -n season-1 --limit 10 # composite, equal weights
snug leaderboard top -n season-1 --limit 10 -w score:0.7,accuracy:0.3
snug leaderboard metric-top -n season-1 -m accuracy --limit 10 # one metric only
snug leaderboard top -n season-1 --page 2 # ranks 11-20
snug leaderboard rank -n season-1 -i player-bob # one entity's standing
snug leaderboard surrounding -n season-1 -i player-bob -w 2 # neighbors (-g for weights here)Weights are relative and need not sum to 1. The arithmetic, with alice at score:100, accuracy:80 and bob at score:120, accuracy:40:
equal weights rank alice first (180 vs 160), but -w score:0.7,accuracy:0.3
flips the order - bob scores 120*0.7 + 40*0.3 = 96 against alice's 94.
Entities with equal composites rank in reverse id order, the same way on
every ranking endpoint, so paging walks a fixed order.
Over HTTP the same query is GET /api/v1/leaderboard/{namespace}/top:
curl -H "Authorization: Bearer $SNUG_API_TOKEN" \
"http://localhost:4000/api/v1/leaderboard/season-1/top?limit=2"{
"status": 200,
"msg": "OK",
"data": {
"entries": [
{
"id": "player-alice",
"rank": 1,
"score": 180.0,
"metrics": { "score": 100.0, "accuracy": 80.0 },
"meta": { "clan": "wolves" }
},
{
"id": "player-bob",
"rank": 2,
"score": 160.0,
"metrics": { "score": 120.0, "accuracy": 40.0 }
}
],
"page": { "has_more": false }
}
}score is the composite; metrics holds the raw values. The ranking
endpoints (top, {metric}/top, time-window, aggregate) share the
standard list contract for paging only:
limit (10 by default, 100 at most) with cursor from page.next_cursor
or a 1-based page. They are uncounted, so total=true is a 400, and
offset is refused like everywhere else. Querying a
namespace nobody has submitted to returns empty entries with 200, not a
404. rank for an unknown entity is 200 with data: null over HTTP;
the CLI prints a not-found notice and exits non-zero.
Time windows
time-window ranks only entities whose last submission falls inside a Unix
epoch range, in seconds - millisecond timestamps do not error, they
silently match nothing. Without -s the window starts 24 hours before its
end, and without -e it ends now:
snug leaderboard time-window -n season-1 -s 1787920000 -e 1787923700 --limit 25aggregate (GET /api/v1/leaderboard/aggregate?namespaces=a,b&metrics=score)
ranks entities by their summed metrics across several namespaces, or, with
group_by, groups one namespace's entities by a metadata field and sums
their metrics per group:
snug leaderboard aggregate -n season-1,season-2 -m score
snug leaderboard aggregate -n season-1 -m score -g clangroup_by takes exactly one namespace and no weights; either mistake is a
400.
Manifests
snug leaderboard create --name "Season 1" --description "Spring" --tags game:arena,season:1
snug leaderboard list --query season --filter 'active[eq]=true' --sort -created_at
snug leaderboard get --id jgYBepqMbqVduynCZGAp
snug leaderboard update --id jgYBepqMbqVduynCZGAp --active false
snug leaderboard delete --id jgYBepqMbqVduynCZGAp --forceManifests inherit their namespace authority. Updates require an editor; deletion
requires a manager. A caller without the required authority is denied, and an
unreadable or missing manifest is 404 leaderboard_not_found. Tags are key:value
pairs, unlike the plain labels in KV.
Introspection and cleanup
snug leaderboard namespaces # readable namespaces + entity counts
snug leaderboard metrics -n season-1 # per-metric count/min/max/avg
snug leaderboard delete-entity -n season-1 -i player-bob # from ALL metrics
snug leaderboard delete-metric -n season-1 -m accuracy # for ALL entities
snug leaderboard delete-namespace -n season-1 --force # everything, no undoThere is no way to remove one entity from a single metric. Deleting a metric immediately changes composite rankings - the remaining metrics carry the whole score.
Limits and configuration
By default 100 namespaces, 10 metrics per namespace (an 11-metric submit
fails with 429 metrics_limit_exceeded, verified), 1000 entries per
metric, and 500 submissions per batch - all tunable via the
LEADERBOARD_* variables.
Reference
- Leaderboard API - score submission and ranking endpoints
- Leaderboard Manifest API - manifest CRUD
- Related: Tournament for brackets, Matchmaking for skill-based pairing, Reputation for event-driven reputation scores
