Bantam — Plug a leaderboard into any game. Done.
Skip the server engineering. Bantam is a single Docker container that owns identity, validation, ranks, and recovery — so your game sends evidence and reads leaderboards. No DB schema. No migration plan. No auth library. No replica math.
What you get
Section titled “What you get”One container
Fastify, SQLite, Litestream. The whole thing runs as one non-root container. SQLite WAL means single-writer per game, no distributed locks, no surprises.
Game-agnostic by default
The framework ships zero game-specific code. Drop a .mjs adapter file into a mounted folder, or skip code entirely and declare boards in JSON. Done.
Server-side authority
Players submit evidence. The server only writes what the adapter derives. Tampering, replays, and fabricated scores are caught at the door.
Self-host or close to it
Docker image, persistent volume, S3-compatible bucket. You own the data. You own the keys. You own the backups.
Your first game — 4 commands, no code required
Section titled “Your first game — 4 commands, no code required”You’re 60 seconds from a leaderboard. The “demo” game below is a real config that ships with the image. It accepts { "high_score": <number> } and ranks players.
1. Run the container
Section titled “1. Run the container”docker run -d --name bantam \ -p 3000:3000 \ -v bantam-data:/data \ -v ./my-game:/bantam/adapters \ -v ./games.json:/etc/bantam/games.json:ro \ -e BANTAM_GAME_DEMO_IDENTITY_SALT=$(openssl rand -hex 32) \ -e BANTAM_GAME_DEMO_SESSION_SECRET=$(openssl rand -hex 32) \ -e BANTAM_GAME_DEMO_CURSOR_SECRET=$(openssl rand -hex 32) \ bazokhan/bantam:latest2. Declare your game
Section titled “2. Declare your game”./games.json:
{ "environment": "production", "games": [ { "id": "my-game", "displayName": "My Game", "adapters": [ { "kind": "generic", "boards": [{ "id": "high_score", "direction": "higher", "period": "all_time" }] } ], "secretPrefix": "BANTAM_GAME_DEMO", "gracePeriodHours": 24, "authProvider": "development" } ]}3. Submit a score from your game
Section titled “3. Submit a score from your game”curl -X POST http://localhost:3000/v1/games/my-game/runs/generic:high_score \ -H "Authorization: Bearer <access-token>" \ -H "Content-Type: application/json" \ -d '{"runId":"run-0001","payload":{"high_score":42}}'4. Read the leaderboard
Section titled “4. Read the leaderboard”curl http://localhost:3000/v1/games/my-game/leaderboards/high_score \ -H "Authorization: Bearer <access-token>"Done. You have a real, authoritative, server-validated leaderboard for a real game. Zero database tables touched by hand.
What’s NOT in your way
Section titled “What’s NOT in your way”No SQL to write
SQLite migrations are checksummed and applied automatically on first boot. Your game never sees a row.
No auth boilerplate
Drop in authProvider: "pgs-v2" and you get Google Play Games Services v2 server auth codes → per-game HMAC pseudonyms in one config line.
No ops math
Litestream replication to S3-compatible storage runs out of the box. npm run ops -- restore demo brings a game back from a fresh bucket.
No proprietary lock-in
The OpenAPI spec, SDK source, and adapter contract are all open. Move off when you outgrow it.
8-bit mascot
generica
shape co.
tile shape
Why “Bantam”?
Section titled “Why “Bantam”?”Bantam is a small, scrappy chicken. Bantam is also a small, scrappy game backend. The container is small, the dependencies are small, the integration footprint is small. It does the things leaderboards need and skips the rest.
For AI agents and automation
Section titled “For AI agents and automation”Bantam is structured so an agent can wire a game end-to-end without reading prose.
Surface area (machine-readable):
- OpenAPI 3.0.3 spec at runtime:
GET /openapi.json - Adapter contract TypeScript types:
@bantam/adapter-contract - Generated SDK:
@gamercury/bantam-sdk(BantamClient) - This site’s typed reference: see API and SDK
Action shapes (one JSON file per surface):
games.jsonschema: TypeBox-validated, see Manifest and PGS v2RunBody.payload:Record<string, unknown>, opaque by design — adapters define their own payload contract- Per-game env vars:
${secretPrefix}_IDENTITY_SALT|SESSION_SECRET|CURSOR_SECRET, plus optionalPGS_CLIENT_ID|SECRET
Capabilities (what Bantam does and does NOT do):
- ✓ authenticates players, persists sessions, refreshes them
- ✓ validates run submissions against a server-side adapter contract
- ✓ persists leaderboard entries per
(board_id, period_key, player_id) - ✓ serves paginated leaderboards, player’s rank, around-me
- ✓ continuous backup to S3-compatible storage
- ✗ does not validate the player’s gameplay events (your adapter does)
- ✗ does not provide an admin dashboard beyond read-only inspection
Sandbox expectations:
- The framework writes only to
/data/games/<gameId>/bantam.sqlite. - The framework reads only from
/etc/bantam/games.jsonand/bantam/adapters/. - No outbound network calls except Litestream replication and the configured auth provider.