Shared leaderboard client for BioKEA games. One Supabase project, one scores table, one TypeScript client.
Status: private. Used internally by every
game-*repo under the BioKEA org.
Nine games were each rolling their own Supabase wrapper, their own daily_scores_leaderboard migration, their own anonymous-handle storage. This package centralizes all of that:
- One Supabase project instead of nine
- One
scorestable withgame_idso adding a 10th game is "pick a slug" - One client API —
submitScore,getTopScores,getDailyLeaderboard - Built-in rate limit (10 submissions/min/client) so trust-the-client doesn't mean trust-the-firehose
- Cross-game leaderboards become a SQL query, not a federation problem
- Create a new Supabase project named
biokea-leaderboards - Apply the migration:
supabase db push # or paste migrations/0001_init_shared_leaderboard.sql into the SQL editor - Copy the project URL and the publishable (anon) key into the games'
.envfiles (see below)
That's the entire backend. No edge functions, no auth, no cron.
In each game's .env:
VITE_SUPABASE_URL=https://<project-ref>.supabase.co
VITE_SUPABASE_PUBLISHABLE_KEY=<anon-key>
In each game's package.json:
{
"dependencies": {
"@biokea/leaderboard": "github:BioKEA/biokea-leaderboard-js#main",
"@supabase/supabase-js": "^2.99.2"
}
}In code:
import { BiokeaLeaderboard } from '@biokea/leaderboard';
const lb = new BiokeaLeaderboard({
supabaseUrl: import.meta.env.VITE_SUPABASE_URL,
supabaseKey: import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY,
});
// Submit a daily score
await lb.submitScore({
gameId: 'plasmid-plinko',
mode: 'daily',
seed: '2026-05-02',
score: 14820,
playerHandle: 'sean',
metadata: { character: 'biologist', perfectAntes: 6 },
});
// Read today's top 10
const top = await lb.getDailyLeaderboard('plasmid-plinko');If env vars are missing the client silently no-ops — same behavior the games had before, so dev environments without Supabase configured don't crash.
class BiokeaLeaderboard {
constructor(config: { supabaseUrl: string; supabaseKey: string });
get configured: boolean;
submitScore(submission: ScoreSubmission): Promise<SubmitResult>;
getTopScores(opts: {
gameId: string;
mode: string;
seed?: string;
limit?: number;
}): Promise<LeaderboardEntry[]>;
getDailyLeaderboard(
gameId: string,
dateKey?: string, // YYYY-MM-DD, defaults to today UTC
limit?: number,
): Promise<LeaderboardEntry[]>;
getPlayerRank(opts: {
gameId: string;
mode: string;
seed?: string;
score: number;
}): Promise<number | null>; // 1-indexed; null on error
}SubmitResult is a discriminated union: { ok: true, id } or { ok: false, reason } where reason is 'rate_limited' | 'invalid' | 'network' | 'unconfigured'. Games can use this to show an appropriate UI (e.g. quietly drop on unconfigured, toast on rate_limited).
scores (
id, game_id, mode, seed, score (bigint),
player_handle, client_id (uuid in localStorage),
metadata (jsonb), created_at
)
game_id and mode are short slugs (validated by check constraints). seed is a date string for daily mode, a hash for seeded runs, null otherwise. metadata is the escape hatch for per-game extras (character class, modifiers, kb_called, waves cleared, …).
See migrations/0001_init_shared_leaderboard.sql for the full DDL.
game_id |
Repo |
|---|---|
plasmid-plinko |
game-plasmid-plinko |
pipette-rush |
game-pipette-rush |
evil-henchman-lair |
game-evil-henchman-lair |
codon2048 |
game-codon2048 |
particle-survival-shooter |
game-particle-survival-shooter |
cal-field-lab-collectible |
game-cal-field-lab-collectible |
3d-biodiversity-collect-em-all |
game-3d-biodiversity-collect-em-all |
pore-sequencing-basecalling |
game-pore-sequencing-basecalling |
collider-tower-defense |
game-collider-tower-defense |
See docs/migration-plan.md. Per-game it's:
- Drop
supabase/migrations/*_create_daily_scores_leaderboard.sql - Replace
src/lib/supabase.tswith an import of@biokea/leaderboard - Update score-submission and leaderboard-display call sites to the new API
- Delete the now-unused
@supabase/supabase-jsdirect usage
MIT — see LICENSE.
Made by BioKEA.