Leaderboards

Declare a best-score or fastest-time board, submit records with the SDK, and read the standings.

Every game has a main score board fed by gameOver. Named leaderboards are for everything else worth ranking, such as a best time per stage. Start with an all-time board using the default best operator. They are declared in remix.json and synced on publish. Remix keeps and ranks the records; your game reads the standings and draws them.

Declaring Boards

{
  "leaderboards": [
    {
      "key": "stage-1",
      "name": "Harbor Sprint",
      "sort": "asc",
      "operator": "best",
      "metadata": { "format": "time_ms" }
    }
  ]
}
FieldValuesMeaning
keyslugStable id the game submits to.
nametextDisplay name, returned with the standings. May change later.
sort"desc" (default), "asc"asc ranks lower values higher. Submit raw milliseconds for times; never invert.
operator"best" (default)Keeps the player's best score.
metadataobjectFree-form display hints such as format. May change later.

sort, operator and reset are immutable once a board ships. A changed value is ignored with a publish warning; only name and metadata update. An undeclared key is refused at runtime, never auto-created.

Submitting a Record

// One id per action; keep it for retries of that action.
const submissionId = sdk.leaderboards.createSubmissionId()

const result = await sdk.leaderboards.writeRecord({
  leaderboard: 'stage-1',
  score: Math.round(lapSeconds * 1000),
  submissionId,
})

if (result?.ok) {
  showBanner(`Best time: ${result.score} ms`)
}

writeRecord resolves { ok: true, score, subscore, numScore } with the value after the operator applied, so a best board answers the previous best when the new value did not beat it. A write never ranks; read the board for a position. A refusal is { ok: false, code, reason }, where code is one of:

  • invalid: a value that is not a safe integer, or an expired submission id.
  • not_found: a board key your remix.json does not declare.
  • conflict: a submission id already used with different values.
  • throttled: too many writes; the reason says when to retry.
  • unavailable: the platform could not answer; retry with the same submission id.

Three rules, each load-bearing:

  • It does not end the run. Submit at the moment it happens, a stage finish mid-run is fine, and still call gameOver once per run. The main score, achievements and crowns ride gameOver, never a board.
  • Every call may resolve null for guests and hosts that predate boards. Show nothing extra and never block gameplay or a results screen on it.
  • Integers only. Milliseconds for times. Unsafe integers and overflow are refused, never rounded.

Submission IDs

A submissionId names one logical action. Reuse it only to retry that action with identical values, for up to seven days. The SDK generates one when omitted, but then every call is a separate submission. An id reused with different values is refused, an expired id is refused rather than reapplied, and an expired action must not be given a fresh id.

Reading Standings

const top = await sdk.leaderboards.listRecords({ leaderboard: 'stage-1', limit: 10 })

listRecords resolves { leaderboard, records, ownerRank, rankingUpdatedAt } where a record is { rank, score, subscore, username, imageUrl, isOwner }. limit defaults to 50 and is at most 100; around the player it defaults to 5 and is at most 50. Draw imageUrl with an <img> and a letter fallback, since it can be null or an image the browser cannot load.

  • listRecords is the top of the current window. ownerRank is exact only when the player's row is on the returned page, otherwise null.
  • listRecordsAroundOwner is a window centred on the player, with an exact ownerRank. Use it for "you versus your neighbours".
  • listFriendRecords ranks the player and everyone they follow among themselves; ownerRank is their place in that set. Nearby-player and friends views are optional; a basic board only needs listRecords.

Every page is one read: each rank is the record's exact position when the page was read, and rankingUpdatedAt is that read's time. The top page can trail a new record by about ten seconds. Read on a user action such as opening a standings screen, not on a timer; a game that polls is throttled on its own boards.

Existing Integrations

Other operators (set, incr, decr), scheduled resets, nearby-player queries and friends queries remain supported. Keep an existing board's configuration when updating a game. New boards can use best without a reset schedule or custom ranking metadata beyond a display format.

Limits

Each game can declare a limited number of boards, and never more than 32. A declaration past your game's limit is refused at publish with a warning that states the limit. See Limits.