gipity add leaderboard adds leaderboards to any game with a database (web-fullstack or api): four functions, six tables, and a small browser helper. It fits high scores, best times, speedruns, puzzle solves and racing alike. Native games call the same functions over HTTPS (the Gipity Godot addon at https://github.com/GipityAI/registry/tree/main/examples/godot wraps them; see steam-game).

1. Declare the boards

Boards live in an app migration, so they ship with the code. Each board's columns are its server-side checks:

-- migrations/001-boards.sql
INSERT INTO lb_boards (board, sort, tiebreak_sort, periods, min_score, max_score) VALUES
  ('arcade:score',   'desc', NULL,  ARRAY['all', 'day', 'week'], 0, 5000000),
  ('puzzle:classic', 'desc', 'asc', ARRAY['all', 'season'],      0, 10000)
ON CONFLICT (board) DO UPDATE SET sort = EXCLUDED.sort, tiebreak_sort = EXCLUDED.tiebreak_sort,
  periods = EXCLUDED.periods, min_score = EXCLUDED.min_score, max_score = EXCLUDED.max_score;

Seasons. A board with season in periods ranks within the running season: the latest lb_seasons row whose window contains now. Declare them in a migration:

INSERT INTO lb_seasons (name, starts_at, ends_at) VALUES ('Season 1', '2026-10-01', '2027-01-01') ON CONFLICT (name) DO NOTHING;

Name boards <mode>:<kind> (e.g. arcade:score, level-3:time, oval-1:lap). Separate variants (a car, a character, a difficulty) are separate boards, or send the variant in meta.

2. Submit and read

Submitting needs a signed-in player: Sign in with Gipity on the web, or a Steam or guest player in a native game. Reading is public.

import { submitScore, top, aroundMe, myEntry, friends, ghost, seasons } from '@gipity/leaderboard';

const r = await submitScore('puzzle:classic', 9200, { tiebreak: 61400 });
// { accepted: true, improved: { all: true, season: true }, period: 'all', personalBest, rank, entryId }
// or { accepted: false, reason, code? }  - a rejection is a normal result, not an error;
// version refusals carry code GAME_VERSION_TOO_OLD | GAME_VERSION_INVALID

await top('arcade:score', { period: 'day', limit: 10, offset: 0 });   // { entries, total, period }
await aroundMe('arcade:score', { radius: 3 });                        // { entry, entries }
await friends('arcade:score', steamFriendIds);                        // Steam ids, you included
await top('puzzle:classic', { period: 'season' });                   // or 'season:Season 1'
await ghost(entryId);                                                  // { ghost: base64 }
await seasons();                                                       // { current, seasons }

Over HTTPS: POST /api/<appGuid>/fn/leaderboard-submit with the same body, and POST /api/<appGuid>/fn/leaderboard-read with { action: 'top' | 'around' | 'me' | 'friends' | 'ghost' | 'boards' | 'seasons', ... }.

Racing boards add splits, a ruleset and a ghost: ('oval-1:lap', 'asc', 12000, 90000, 3, ARRAY['a1f09c']) for (board, sort, min_score, max_score, splits, rulesets), then submitScore('oval-1:lap', 31250, { ruleset: 'a1f09c', splits: [10400, 21010, 31250], ghost }).

3. Moderate

leaderboard-admin is auth: member (the owner and project members only):

gipity fn call leaderboard-admin '{"action":"submissions","board":"arcade:score","rejectedOnly":true}'
gipity fn call leaderboard-admin '{"action":"ban","userGuid":"u_...","reason":"impossible score"}'
gipity fn call leaderboard-admin '{"action":"remove","entryId":"lbe_..."}'
gipity fn call leaderboard-admin '{"action":"reset","board":"arcade:score","period":"day"}'

Also unban and bans. A ban deletes the player's entries and hides them from every view.

Deleted players are erased automatically. The kit's leaderboard-player-deleted function is declared with hooks: [user_deleted], so when an app player deletes their account (DELETE /api/<appGuid>/auth/player) the platform runs it first: it deletes their entries (which carry their display name), their submission log, any ban, and the ghosts only they used. If it fails, the player is not deleted and the call returns 502 PLAYER_CLEANUP_FAILED, so the game can retry; see why with gipity logs fn leaderboard-player-deleted. Nobody else can call it (it's auth: member and refuses calls the platform didn't trigger). To erase someone by hand (e.g. a Gipity-account user who asked), use gipity fn call leaderboard-admin '{"action":"purge","userGuid":"u_..."}'.

Limits of the checks

These are plausibility checks. A careful cheater can submit a believable fake, so review the rejected-submissions log and ban. Replay bytes aren't inspected. Real cheat resistance needs the game to run on a server, which this kit doesn't do.

Common mistakes