Board config
What each field decides is on boards and windows; what the rank field costs is on what it costs.
| Field | Default | |
|---|---|---|
key | — | A URL path segment, and the entry key |
store | d1 | d1 only, today |
direction | — | desc (highest wins) or asc (lap times). Immutable |
aggregation | — | best · latest · sum. Immutable |
window | — | CRON, UTC. Omit for all-time. Immutable |
retain | keep all | Closed windows to keep |
retainDays | keep all | Delete older than N days. Set one, not both |
min, max | — | Server-side bounds |
trackActivity | false | The cost lever. True writes on every submission |
tiers | — | Classified on read |
serverAuthoritative | true |
store, direction, aggregation and window are immutable once a board has an entry. Changing one raises leaderboard/board_immutable on the next submission. A new definition means a new key.
Top-level config
| Key | Default | |
|---|---|---|
rank | "live" | Or { materialize: "<cron>" } |
adminScope | The hide and remove routes | |
submitScope | Required when server-authoritative | |
basePath | /leaderboard |
Routes
| Route | Verified by |
|---|---|
GET /leaderboard | user |
POST /leaderboard/:board | user + submit scope when server-authoritative |
GET /leaderboard/:board/top | user |
GET /leaderboard/:board/me | user |
GET /leaderboard/:board/around | user |
POST /leaderboard/:board/segment | user |
PUT /leaderboard/:board/me/visibility | user |
PUT /leaderboard/:board/entries/:userId/hidden | user + admin scope |
DELETE /leaderboard/:board/entries/:userId | user + admin scope |
Any read takes ?window=<key> to read a closed window.
There is no public surface. Without auth composed, every route denies. Using Leaderboard has the calls.
Limits
| Segment members per query | 80 — D1’s 100-parameter cap, less 10 the my-rank path spends |
| Rank refresh chunk | Paces the walk, not a row count for one statement |
| Stale lock reclaim | 1 hour, via LEADERBOARD_LOCK_STALE_MS |
| Storage | ~3 GB at 10M players, against a 10 GB cap |
Tie-breaking
Score, then earliest achievedAt, then user id.
Total ordering — no two entries tie, so dense-versus-competition ranking never arises and neither is implemented.
Resubmitting the same score does not reset achievedAt.
Error codes
Each is domain/reason, like every error code in the kit.
| Code | |
|---|---|
leaderboard/board_not_found | |
leaderboard/entry_not_found | |
leaderboard/board_immutable | One of the four fixed fields changed |
leaderboard/invalid_schedule | The CRON did not parse |
leaderboard/score_rejected | Outside min/max |
leaderboard/submit_forbidden | Server-authoritative, and no submit scope |
The worker
A cron-triggered Workflow doing the retention sweep and, if materialized, the rank refresh.
Needed when rank is materialized, or any board configures retention. A live board set keeping everything needs no worker.
It need not be separate — a Workflow class, a scheduled handler and one cron trigger fold into your app Worker. A cron trigger is the only hard requirement.
A D1 advisory lock keeps one refresh running at a time; a second instance skips rather than interleaving.
What is not implemented
Dense ranking and competition ranking, because the ordering is total and neither can arise.
Any store but D1, today.
A board admin surface — boards are config.