Leaderboards

A leaderboard is a named, ranked list of players and scores, scoped to your project. Your room posts to it; anyone can read it.

// in your room
rpc: {
  finishRound(state, _params, ctx) {
    const score = state.players.get(ctx.clientId)?.score ?? 0;
    void ctx.room.leaderboard.submit('weekly', ctx.playerId, score);
  },
}
// on your website, or anywhere else
const res = await fetch('https://irt.io/v1/leaderboard/<project>/weekly/top?limit=10');
const { entries } = await res.json();

Only your room can write

There is no client-side submit. Not a discouraged one, not a signed one — there is no API for it anywhere in the SDK, and no HTTP endpoint a browser could call. A score reaches a board along exactly one path: your room code calls room.leaderboard.submit, and the platform carries it the rest of the way with a credential no client has.

Two checks happen outside your tenant on the way:

  • The player has to be in the room. Submitting for ctx.playerId always works; submitting for an id you made up is refused with E_LB_NOT_IN_ROOM.
  • The project is taken from the machine credential, never from anything the room says, so a room cannot write to another project’s board.

What that does and does not buy you

It means a player cannot post 999999 from the browser console, because there is nothing to post to. That is worth a great deal and it is not the same as cheat-proof.

If your room trusts a number a client wrote into its own entity and forwards it to a board, the board will faithfully record that number. Server-authoritative means your server decides; it does not mean your server decided correctly. Compute the score from state you control.

Anti-abuse on irtio is server authority plus rate limits. There is nothing else behind that sentence — no heuristics, no anomaly detection, no shadow bans. If a board matters enough to be worth attacking, design the room so the score is not the client’s to influence.

Best score wins

A submit replaces a player’s stored score only if it is better. Three things follow, and all three are load-bearing:

  • A worse score cannot knock a player down. Submit freely at the end of every round.
  • The same score submitted twice changes nothing at all, so a retry or a duplicate is harmless. There is no “did I already post this” bookkeeping for you to get wrong.
  • The response says whether your submit is the one that now stands, so you can tell a player they beat their record without reading the board back.

Ties break by who got there first.

Direction

Boards are higher-is-better unless you say otherwise. For times, lap counts and stroke counts, say otherwise:

curl -X PATCH https://irt.io/v1/projects/<id> 
  -H 'content-type: application/json' 
  -d '{"name":"my game","boards":{"lap-times":{"direction":"lower"}}}'

Direction belongs to the board, not to a score, so a board cannot end up with rows that disagree about which of them is winning.

Scores are whole numbers

Integers only. A float score is refused with E_LB_BAD_SCORE.

This is a decision, not a limitation we mean to lift: floats produce ties that are not really ties, they drift between the JSON your room sends and the value stored, and “why is my 0.1 + 0.2 in second place” is a support thread nobody should have. Submit milliseconds, centimetres, or points.

Reading

Two shapes, both public — no key, no session, no cookie. A leaderboard on a marketing page is a normal thing to want.

GET /v1/leaderboard/<project>/<board>/top?limit=10
GET /v1/leaderboard/<project>/<board>/around/<playerId>?window=5

top is the wall of fame. around is the read most of your players actually care about: their own rank, and the handful of people just above and below them. Ranks are absolute in both, so you can print “you are 4,281st” from any slice.

A player who has never scored gets {"rank": null, "entries": []} rather than a 404. Not being on a board is an ordinary state.

From a terminal:

irtio leaderboard weekly --limit 20
irtio leaderboard weekly --around irt:9tW2rQ...

There is no irtio leaderboard submit, for the reason at the top of this page.

Identity

A board is only as durable as the ids it ranks. With a key join, ctx.playerId is the client id, which expires with the resume token — so yesterday’s leader is a stranger today. Give your players an identity (or your own JWT) and their place on the board survives reconnects, hibernation and closing the tab.

Limits

One million rows per project across all boards, and reads are rate limited per address and per project — see the limits page. Board names are 1 to 64 characters of a-z 0-9 . _ -.

Rotating boards — daily, weekly, seasonal — are not built. The storage has room for them, so adding them later will not move anyone’s existing scores, but today a board is all-time. A weekly board today means a new board name each week and reading the one you want.