The open-source backend behind cheddaboards.com. Use the hosted service, or run your own.
Leaderboards, achievements, player accounts, anti-cheat and moderation for games, running as a single canister on the Internet Computer.
This is the backend canister: the on-chain logic and storage behind every CheddaBoards game.
| Path | What it is |
|---|---|
src/cheddaboards_v2_backend/main.mo |
The canister actor: games, scoreboards, sessions, achievements, anti-cheat, moderation, admin |
src/cheddaboards_v2_backend/ (types, apikeys, players, scoreboards) |
Supporting Motoko modules imported by main.mo |
dfx.json |
Build config, as used for the production canister |
CHANGELOG.md |
Per-release notes, including upgrade notes for self-hosters |
The Candid interface is generated from the source when you build (dfx generate).
This public repo is a mirror of the private source, published one commit per release, so it can trail the live canister by a release.
Not in this repo:
| Looking for | Go to |
|---|---|
| Documentation, quick starts, REST reference | docs.cheddaboards.com |
| The REST proxy behind api.cheddaboards.com | cheddaboards-proxy |
| Godot 4 add-on (4.3+) | cheddaboards-godot-addon, also on the Godot Asset Store |
| Godot 4 full game template | cheddaboards-godot, also on the Godot Asset Store |
| Godot 3.6 add-on | cheddaboards-godot3-addon |
| Unity SDK | cheddaboards-unity |
| A worked example | Dodge the Creeps × CheddaBoards (play it) |
| Service status | status.cheddatech.com |
Every SDK talks to the same REST API, so anything that can make an HTTP request works too: Unreal, GameMaker, Bevy, native mobile, web, or your own engine.
Games talk plain HTTPS to a thin REST proxy. The proxy checks the game's API key or the player's sign-in, then calls the canister using its own signing identity, the verifier. The canister stores everything and enforces the rules.
The trust model, in short:
- Writes go through the verifier. Sign-in, session minting, and every score, achievement and nickname change for API-key players are accepted only from the verifier principal. Calling the canister directly for these returns
Unauthorized. - Signed-in developers and players act through sessions. A session token is checked by the canister on every call, lasts 30 days, and renews on use.
- Board reads are public and can skip the proxy. The canister serves scoreboards over HTTP itself (see Direct HTTP reads).
- Lookups that could leak information are closed. API key validation, lookups by email address and lookups by API-key player ID are verifier-only.
- Sign-in only trusts the platform's own OAuth credentials. The proxy accepts Google and Apple tokens issued for its own client IDs and bundle ID, never ones a developer registers.
The proxy that runs api.cheddaboards.com is open source too, at cheddaboards-proxy. Self-hosters can run it or build their own against the Candid interface (see Self-hosting).
- Leaderboards: server-validated scores, with all-time, weekly and daily boards created for every game
- Timed scoreboards: daily, weekly, monthly and custom-interval boards that reset on calendar boundaries and archive each period
- Category scoreboards: per-level, per-mode or per-category boards you submit to by ID, without registering a separate game for each
- Board limits: per-game caps by developer tier, checked when a board is created: free 50 boards with at most 5 fan-out, pro 250 with at most 15. Targeted boards count only toward the total
- Player accounts: anonymous players identified by device under the game's API key, plus Google, Apple and Internet Identity sign-in. Device-code login for linking a game client to an account is implemented in the hosted proxy, not in the canister
- Account linking: an anonymous profile merges into a signed-in account without losing progress. Scores and streaks merge by maximum, achievements are combined, play counts are summed
- Cross-game profiles: a signed-in player keeps one identity across every CheddaBoards game
- Achievements: per-player unlocks, with a batch call for unlocking many at once
- Anti-cheat: per-player rate limiting, per-round and absolute score caps, optional play-session time validation, and a suspicion log the game owner can read, with players shown only as opaque keys
- Moderation: game owners can delete a single score entry or wipe a player from their boards, with a capped audit log that stores player identifiers as hashes
- Game metadata: play URL, website, engine tag and an optional developer contact email
- API keys: one key per game, generated from IC randomness, revoked automatically when a game is removed
- Safe deletion: deleted games can be recovered for 30 days, or the owner can remove one for good straight away. Permanent removal clears the game's boards, scores, archived periods and API key
- Operations: a
memStatsquery and aGET /metricsendpoint for capacity monitoring, and an admin command set for support and recovery
You don't need to run anything. Register a game at cheddaboards.com/developers, copy its API key, and submit a score:
curl -X POST https://api.cheddaboards.com/scores \
-H "Content-Type: application/json" \
-H "X-API-Key: YOUR_API_KEY" \
-H "X-Game-ID: your-game-id" \
-d '{"playerId":"player_123","gameId":"your-game-id","score":1200,"streak":3}'The hosted service is free: 3 game slots to start (more on request from the dashboard), up to 50 boards per game, unlimited players, no per-player fees.
For Godot and Unity, use an SDK from the table above. Setup guides, the REST reference and error codes are at docs.cheddaboards.com. AI coding assistants can start from docs.cheddaboards.com/llms.txt.
The canister is fully open source and you can run your own instance. It is more assembly than a one-click template: you deploy the canister, then run an API layer in front of it, either cheddaboards-proxy or your own. The self-hosting guide covers the proxy side in more detail.
- dfx (the IC SDK). Production is built with dfx 0.32.0
- Cycles to run a canister on mainnet
- Basic familiarity with Motoko and the Internet Computer
git clone https://github.com/cheddatech/cheddaboards.git
cd cheddaboardsOpen src/cheddaboards_v2_backend/main.mo and replace the placeholder principals (aaaaa-aa) with your own:
// Your proxy's signing identity: the only principal allowed to make
// privileged calls. REPLACE with your own.
private transient let VERIFIER_PRINCIPAL : Text = "aaaaa-aa";
// Super admin (your dfx identity). REPLACE with your own.
private var CONTROLLER : Principal = Principal.fromText("aaaaa-aa");
// Bootstrap admin in postupgrade() (can be the same as controller). REPLACE.
let firstAdmin = Principal.fromText("aaaaa-aa");Get your principal with dfx identity get-principal. A quick check before any deploy: grep -n "aaaaa-aa" src/cheddaboards_v2_backend/main.mo should print nothing.
Board limits per game are set by the FREE_MAX_BOARDS, FREE_MAX_FANOUT_BOARDS, PRO_MAX_BOARDS and PRO_MAX_FANOUT_BOARDS constants in main.mo. They are transient, so you can change them on any upgrade.
⚠️ Set these before your first deploy. The actor ispersistent, so top-level variables are stable and survive upgrades. Editing a literal afterwards does not change the running value.VERIFIER_PRINCIPALis re-applied inpostupgrade(), so it can be rotated by upgrading.CONTROLLERis not, so get it right the first time.
# Local testing
dfx start --background
dfx deploy
# Mainnet, first install
dfx deploy --network icThe canister uses enhanced orthogonal persistence (--enhanced-orthogonal-persistence in dfx.json).
dfx canister --network ic stop cheddaboards_v2_backend
dfx canister --network ic snapshot create cheddaboards_v2_backend
dfx deploy cheddaboards_v2_backend --network ic --mode upgrade --wasm-memory-persistence keep
dfx canister --network ic start cheddaboards_v2_backend- Read the changelog first. Each release in CHANGELOG.md has notes for self-hosters. Some upgrades need two steps, and removing a stable field needs a one-shot migration.
- Snapshot before every upgrade. It is your only rollback. Each snapshot is a full copy of the heap and counts towards memory and cycle burn, so keep one at a time (
snapshot create --replace <id>) and delete it once the upgrade is proven. - Check the module hash afterwards.
dfx canister --network ic info <name>should match theshasum -a 256of the wasm you built.
The simplest route is cheddaboards-proxy, the same code that runs api.cheddaboards.com. Its README covers the environment variables and deployment. To build your own instead, generate the Candid bindings it will call:
dfx generate cheddaboards_v2_backendEither way, your API layer translates HTTP requests into canister calls. It needs:
- A signing identity (for example an Ed25519 keypair) whose principal you set as
VERIFIER_PRINCIPAL. Keep the private key secret: it can mint sessions for any user and write scores for any game - To sign every canister call as that identity, queries included. External score submits, achievement unlocks, play-session starts and API key lookups are all rejected or return nothing for any other caller
- To validate each game's API key itself, through
validateApiKeyQuery, on every request. The canister trusts the proxy to have done this for API-key players, so never accept a key without asking the canister - To verify OAuth tokens itself (for example via JWKS) before calling
socialLoginAndGetProfile, and to accept only tokens issued for your own client IDs and bundle ID. Sessions are tied to the email, not the game, so accepting a developer-supplied audience would let that developer sign in as anyone who uses their app - Rate limiting, since the canister only applies a per-player submit throttle
- Your canister ID and an IC host (
https://icp-api.io, orhttp://127.0.0.1:4943for local dfx) - OAuth client IDs for the providers you support
- A CORS policy. Games run on arbitrary domains, so the hosted proxy reflects the request origin; that is safe only because auth rides on headers, never cookies
After upgrading to v0.13.0 or later, run adminGate("sweepOrphanApiKeys", []) once to report API keys left behind by deleted games, then again with ["confirm"] to revoke them.
The Candid interface is the full contract. Every deployed canister gets the Candid UI for free. Here it is against the live production canister:
Key methods by area. Most owner-facing methods come in two forms: a principal variant (the caller is the owner) and a BySession variant (a session token identifies the owner).
Sign-in and sessions: socialLoginAndGetProfile, createSessionForVerifiedUser, iiLoginAndGetProfile, validateSession, destroySession
Account linking: migrateAnonymousAccount, migrateAnonymousToII (both verifier-only)
Games: registerGame, updateGame, deleteGame, recoverDeletedGame, permanentlyDeleteGame, getGame, listGames
Game metadata: setGameEngine, getGameEngines, setGameWebsite, getGameWebsites, setDeveloperContact, getMyDeveloperContact
API keys: generateApiKey, getApiKey, revokeApiKey, validateApiKeyQuery (verifier-only)
Scores and leaderboards: submitScore, submitScoreToBoard, getScoreboard, getScoreboardsForGame, getLeaderboard, getPlayerRank, getPlayerScoreboardRank
Scoreboard management: createScoreboard, updateScoreboard, resetScoreboard, deleteScoreboard, getBoardLimitsBySession
Archives: getScoreboardArchives, getLastArchivedScoreboard, getArchivedScoreboard
Play sessions: startGameSessionByApiKey, startGameSessionBySession, getPlaySessionStatus, cancelPlaySession
Achievements: unlockAchievement, unlockAchievementBatch, getAchievements
Profiles: getMyProfileBySession, getUserProfile, getGameProfile, changeNicknameAndGetProfile, isNicknameAvailable
Moderation: getScoreboardAdmin, removeScoreEntry, removePlayerScores, getEntryDeletionLog, getSuspicionLogBySession
Stats and operations: getSubmissionStats, getSystemInfo, memStats, adminGate
Fan-out vs targeted:
submitScorewrites a score to every fan-out board on the game: the default all-time, weekly and daily boards plus any other non-targeted board.submitScoreToBoardwrites to one board only, which is how per-level and per-category boards work. A board opts in to targeted behaviour in its config, which is fixed at creation. Because every plain submit writes to all fan-out boards, they have their own, lower limit.
User ID types: player-facing methods take a type and an ID. Use
sessionwith a session token,externalwith an API-key player ID, orprincipal. Theexternaltype ongetUserProfile.
The canister answers plain HTTPS on its raw domain, with open CORS, so game clients can read boards without going through a proxy:
# A scoreboard, same JSON as the REST API
curl "https://<canister-id>.raw.icp0.io/games/<gameId>/scoreboards/<boardId>?limit=10"
# Capacity figures: cycles, memory and map sizes, with a warn status
curl "https://<canister-id>.raw.icp0.io/metrics"The production canister ID is fdvph-sqaaa-aaaap-qqc4a-cai.
Found a vulnerability? Please email info@cheddaboards.com instead of opening a public issue; SECURITY.md has the details. Security fixes are listed under a Security heading in the changelog, and self-hosters should upgrade when one appears.
Issues and pull requests are welcome. Because this repo is a published mirror, accepted changes are applied to the source and appear here with the next release, with credit.
- Fork the repo
- Create a feature branch
- Make your changes
- Open a PR describing what it changes and how you tested it
- Website: cheddaboards.com
- Docs: docs.cheddaboards.com
- Developer dashboard: cheddaboards.com/developers
- Status: status.cheddatech.com
- Changelog: CHANGELOG.md
- Games: chedda.games
- Company: cheddatech.com
- X: @cheddatech
MIT. See LICENSE.
Built by CheddaTech Ltd on the Internet Computer.

