A realโtime leaderboard library powered by Redis, Postgres, Socket.IO, and TypeScript.
- Postgres โ Persistent storage
- Redis โ Ultra-fast reads & live score updates
- Socket.IO โ Instant leaderboard broadcasts
- Express Router โ Drop-in REST API
Perfect for multiplayer games, coding contests, and online quizzes.
npm install live-leaderboard redis pg express socket.io dotenv
# or:
pnpm add live-leaderboard redis pg express socket.io dotenvSave this as playground/schema.sql in your project:
CREATE TABLE IF NOT EXISTS leaderboard_scores (
id BIGSERIAL PRIMARY KEY,
game_id TEXT NOT NULL,
user_id TEXT NOT NULL,
score INTEGER NOT NULL,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT leaderboard_scores_game_user_uk UNIQUE (game_id, user_id)
);
CREATE INDEX IF NOT EXISTS idx_leaderboard_scores_game_score_desc
ON leaderboard_scores (game_id, score DESC);Run it once:
psql -d <YOUR_DB_NAME> -f playground/schema.sqlIf you customize table/column names, update the
LeaderboardConfigaccordingly (see below).
.env
REDIS_URL=redis://localhost:6379
POSTGRES_URL=postgres://user:pass@localhost:5432/dbname
PORT=3000Run Redis & Postgres (Docker)
docker run -p 6379:6379 redis
docker run -p 5432:5432 \
-e POSTGRES_PASSWORD=pass \
-e POSTGRES_USER=user \
-e POSTGRES_DB=dbname \
postgresserver.ts
import express from "express";
import http from "http";
import { Server as SocketIOServer } from "socket.io";
import { createClient } from "redis";
import { Pool } from "pg";
import dotenv from "dotenv";
import {
Leaderboard,
RedisService,
PostgresService,
createLeaderboardRouter,
type LeaderboardConfig,
} from "live-leaderboard";
dotenv.config();
async function main() {
const app = express();
const server = http.createServer(app);
const io = new SocketIOServer(server, { cors: { origin: "*" } });
app.use(express.json());
const redis = createClient({ url: process.env.REDIS_URL });
await redis.connect();
const pg = new Pool({ connectionString: process.env.POSTGRES_URL });
const config: LeaderboardConfig = {
redisPrefix: "leaderboard",
tableName: "leaderboard_scores",
columns: {
gameId: "game_id",
userId: "user_id",
score: "score",
},
};
const leaderboard = new Leaderboard(
new RedisService(redis, config.redisPrefix),
new PostgresService(pg, config)
);
app.use("/leaderboard", createLeaderboardRouter(leaderboard, io));
io.on("connection", (socket) => {
console.log("Client connected:", socket.id);
socket.on("join-game", (gameId: string) => socket.join(gameId));
});
server.listen(process.env.PORT || 3000, () =>
console.log(
`Server running at http://localhost:${process.env.PORT || 3000}`
)
);
}
main().catch(console.error);<!DOCTYPE html>
<html>
<head>
<title>Live Leaderboard</title>
<script src="https://cdn.socket.io/4.7.2/socket.io.min.js"></script>
</head>
<body>
<h1>Leaderboard</h1>
<ul id="leaderboard"></ul>
<script>
const socket = io("http://localhost:3000");
socket.emit("join-game", "demo123");
socket.on("leaderboard:update", (players) => {
const list = document.getElementById("leaderboard");
list.innerHTML = "";
players.forEach((p, i) => {
const li = document.createElement("li");
li.textContent = `#${i + 1} ${p.userId} - ${p.score}`;
list.appendChild(li);
});
});
</script>
</body>
</html>Submit or update a score.
Body
{
"gameId": "demo123",
"userId": "alice",
"score": 150
}Validation
gameIdanduserIdmust be non-empty stringsscoremust be a non-negative integer
Response
{ "success": true }Get top N players. limit must be an integer โฅ 1. A hard cap of 100 is enforced.
Response
[
{ "userId": "dave", "score": 490 },
{ "userId": "alice", "score": 237 },
{ "userId": "bob", "score": 39 }
]Get a userโs rank.
Response
{ "rank": 2 }import {
Leaderboard,
RedisService,
PostgresService,
type LeaderboardConfig,
} from "live-leaderboard";
import { createClient } from "redis";
import { Pool } from "pg";
const redis = createClient({ url: process.env.REDIS_URL });
await redis.connect();
const pg = new Pool({ connectionString: process.env.POSTGRES_URL });
const config: LeaderboardConfig = {
redisPrefix: "lb",
tableName: "leaderboard_scores",
columns: { gameId: "game_id", userId: "user_id", score: "score" },
};
const leaderboard = new Leaderboard(
new RedisService(redis, config.redisPrefix),
new PostgresService(pg, config)
);
await leaderboard.submitScore("game1", "bob", 1000);
console.log(await leaderboard.getTopPlayers("game1", 5));
console.log(await leaderboard.getUserRank("game1", "bob"));Use any table/column names by changing the config:
const config: LeaderboardConfig = {
redisPrefix: "myapp:lb",
tableName: "game_scores_custom",
columns: {
gameId: "gid",
userId: "uid",
score: "points",
},
};Matching SQL:
CREATE TABLE IF NOT EXISTS game_scores_custom (
id BIGSERIAL PRIMARY KEY,
gid TEXT NOT NULL,
uid TEXT NOT NULL,
points INTEGER NOT NULL,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT game_scores_custom_gid_uid_uk UNIQUE (gid, uid)
);live-leaderboard/
โโโ src/
โ โโโ config/ # DB & service configs
โ โโโ server/ # Express + Socket.IO setup
โ โโโ sdk/ # Leaderboard classes/services
โ โโโ playground/
โ โโโ server.ts # Example server
โ โโโ schema.sql # Postgres schema
โโโ package.json
โโโ README.md
MIT ยฉ abhi-garg