Follow-up from the Alternative Origins threads: CheddaBoards is live

Hey everyone, some of you might remember me from the Alternative Origins threads late last year. I was trying to get shared auth working across lots of indie game frontends on itch.io, CrazyGames and everywhere else indie games actually get played.

I thought I’d solved it with Alternative Origins, then Timo pointed out the ten origin limit. For a platform that eventually needed to support arbitrary third party games, that pretty much killed the approach. I said I’d give it some more thought. This is the follow up.

Quick origin story first, because it’s a very ICP one. CheddaBoards didn’t start as a product. It started as a leaderboard prototype for a cheese themed game I built for a memecoin project. My first ever canister went up in April 2025. A week later I lost access to the machine holding the identity, and after months of trying I never got it back. The source went with it.

Here’s the part that still gets me. For all I know, that canister is still out there. I can’t upgrade it, administer it or recover its internal state. It was my first real lesson in what ownership on ICP actually means when you’re the one who loses the keys. “Can’t be taken away” cuts both ways. It’s the most ICP lesson I’ve ever learned, and honestly it’s half the reason I rebuilt the whole thing properly instead of walking away.

The rebuild has been live in production since December 2025 and hasn’t stopped since.

So what is it. CheddaBoards is leaderboards as a service for indie game devs. The core backend is a single Motoko canister holding scores, players, sessions, achievements, moderation, the lot. There are open source SDKs for Godot 4, Godot 3.6 and Unity, plus a plain HTTP API for anything else. Someone recently built a C SDK against it that I didn’t write, which was a good day.

The auth problem from those old threads eventually got solved by stepping around it completely.

Instead of trying to make every game an Internet Identity frontend, the game shows a short code. The player opens cheddaboards.com/link in any browser, on any device, approves the code, and the session lives canister side. The game itself never touches OAuth, Internet Identity or Alternative Origins at all.

That one change is what made itch.io, CrazyGames, Quest browsers and random iframes all work with zero per platform setup, and it’s been the single most important architectural decision in the project.

Where it’s at today: 24,201 score submissions from 1,303 players across 67 registered games and roughly 46 developers, with somewhere between six and ten games actively submitting on any given day. Devs are starting to find it organically now. A shipped game in Austria named CheddaBoards in its privacy policy, and that C SDK appeared unprompted on GitHub.

One honest boundary, because I’d rather you hear it from me. The HTTP layer between games and the canister currently runs off chain on Netlify, and that proxy is closed source. The canister and all the SDKs are open, and the backend is on GitHub at https://github.com/cheddatech/CheddaBoards and self hostable if you want to run your own stack.

The public repo can trail the live canister by a release or two because I sync changes back in batches, and games don’t use II directly right now, though the developer dashboard supports it. I know an off chain proxy isn’t the pure answer, which brings me to the next thing.

I’m writing up the proxy to canister question properly: whether moving the HTTP layer fully on chain is realistic now, and what the cleanest architecture looks like for exposing a game facing HTTP API directly from ICP.

Just this week the proxy’s limits bit me in production. Batch achievement unlocks were timing out through the proxy, and the fix was moving the whole batch into a single canister method. Every problem like that pushes more logic onto the canister anyway, which is partly why I want to answer the question properly.

I’d genuinely like input from people here who’ve done HTTP heavy canisters at production traffic levels. That post is coming in the next week or two.

Good to be back

Chedz
cheddaboards.com

Hey @pacemaker86,

thanks for sharing this! I love arcade games and this use case :slight_smile:

:warning: Quick disclosure before you start reading:

  • I ran your public repo (github.com/cheddatech/CheddaBoards) and the Godot SDK
    through an AI coding agent to review the architecture around your proxy-to-canister question, and cross-checked it against the current Internet Identity codebase. Everything below is
    that review; a static read of the open code, not the live canister, which you’ve said runs ahead of the repo.

Is it realistic? Yes.

Every primitive you’d need is available and in production: canister-served HTTP (http_request / http_request_update), certified query responses, custom domains on the boundary nodes, HTTPS outcalls, and on-chain OAuth verification. The one thing to be precise about is what “fully on-chain” means: put all the logic, trust, and secrets on-chain, but keep a thin, stateless edge in front of writes for abuse and cost control. That edge is nothing like your current proxy — no keys, no logic, and optional (if it dies you point the domain straight at the canister). I’ll come back to why it’s needed.

Cleanest target architecture

Custom domain straight to the canister. Point api.cheddaboards.com at the boundary nodes via a custom-domain registration. The path/method/header contract stays identical, so your existing SDKs and the 67 live games don’t change — the migration is invisible to them.

Reads = certified queries, writes = update calls. Serve leaderboards/profiles as certified queries, so responses are verified against the IC root key instead of trusted from a single replica. These run at query speed (~100-300ms) — the certification work happens at write time, not read time, so it’s an engineering cost (maintaining a certification tree), not a latency one. Writes go through http_request_update and consensus (~1-2s). Pragmatic path: ship everything via upgrade=true first (correct, no cert-tree work, ~2s on reads too), then move the hot reads to certified queries.

Authentication via Internet Identity, rather than rebuilding JWT verification. Two recent II changes matter for you: it now does OpenID Connect natively (players sign in with Google/Apple/Microsoft through II, which verifies the token and returns a delegation), and the alternative-origins cap went from 10 to 100. I wouldn’t put games on II directly — its delegations are origin-bound, and your short-code link flow already handles arbitrary game origins better than alt-origins ever could. But cheddaboards.com/link is a single stable origin, i.e. an ideal II relying party: let it authenticate the player via II, and the canister verifies the delegation and mints its session from it. That means you never implement RS256/JWKS verification in-canister, and your off-chain verifier signing key goes away entirely. (If you’d rather own the flow, on-chain JWT verification is also viable — motoko_rsa + motoko_jwt + HTTPS outcalls for JWKS — and f0i/identify does exactly this in production.)

A thin edge for writes. A reverse proxy on the domain (Cloudflare Worker, edge function, nginx) that rate-limits and forwards to the IC gateway. This is the one job the canister can’t do itself: update calls cost cycles, you can’t cache a write, and in-canister every caller looks like the anonymous gateway principal, so there’s no IP to throttle on. An edge drops junk before it costs the canister anything. Keep it stateless and back it with per-key/per-game limits inside the canister as a floor. (The boundary nodes apply baseline DDoS throttling even with no edge; the edge just adds the app-aware, IP-level layer.)

What has to be handled before direct exposure

The migration’s real work isn’t the HTTP layer — it’s that several responsibilities currently sit in the proxy and have to move into the canister first, because today the proxy is the only caller:

  • Authorization on the write path. Right now the canister trusts the proxy to have authorized external score submissions. That check has to exist in the canister before it’s internet-facing.
  • Credential entropy. Session tokens and API keys need to be unguessable and stored hashed — canister state is visible to every node provider on the subnet.
  • Read path. Add pagination and a working cache to the leaderboard query, then certify it. As written it does a full scan per call; your proxy cache has been absorbing that, and served directly it will hit the query instruction limit as you grow.
  • Player emails. You currently store raw emails as the identity key. On replicated state that’s node-provider-visible, so hash the join key and, ideally, don’t persist the plaintext at all — with II attribute sharing, II supplies the verified email per session and the canister only needs the hash. (vetKeys only if you genuinely need recoverable plaintext on-chain.)
  • Upgrade safety. The canister keeps state in transient maps and serializes everything to stable arrays in preupgrade/postupgrade. That serialization has a fixed instruction limit, so as data grows the upgrade itself will eventually trap — the one failure you least want. Modern Motoko orthogonal persistence removes the hooks entirely: hold state in persisted let collections (mo:core Map/List) and there’s no serialize step to hit the limit. Worth doing before traffic grows; canister sharding is the longer-term lever.

A couple of these touch the live service, so I’ll send you the specific line-level findings separately rather than post them in-thread.

On the API key model

Embedding one key in the game for all players is fine and normal — a client-side key is a public identifier, not a secret (same as Firebase/PlayFab), and neither on-chain nor a proxy can change that. Treat it as a coarse project/quota gate; real integrity comes from your server-side validation plus per-player identity, so a forged score is attributable to a bannable player rather than to a shared key. Binding submissions to an II-backed player session is the strongest version of that. For studios running their own backend, a separate secret key path lets their submissions actually be trusted.

Honest tradeoffs

  • Latency: reads stay fast with certified queries; what you give up is edge-cache speed (tens of ms → ~100-300ms) and write latency (~1-2s through consensus — usually fine, since submits happen at game-over and your device-code polling is already on a 5s cadence).
  • Cost/abuse: you pay cycles for write traffic including junk — the reason for the thin edge.
  • Privacy: replicated state is node-provider-visible, so PII needs the deliberate handling above.

On balance this is a net security improvement — it removes your most sensitive secret (the off-chain verifier key) and makes auth consensus-verifiable — provided the authorization and credential work lands before you expose anything. Happy to go deeper on any piece.

As noted by the agent, I will send you some more details in DM.

Regarding II authentication I am also tagging @sea-snake here :slight_smile:

I am looking forward to future updates of this!

Best,
Marco

One follow-up: I see that you are still using dfx. You should consider switching to icp-cli rather sooner than later :slight_smile:

https://cli.internetcomputer.org/1.3/migration/from-dfx

If you have any questions regarding this, let me know.

When building with AI agents you might also want to check out the ICP Skills. Specifically the icp-cli, writing-motoko and migrating-motoko-actors skills. Ideally your agent is aware of all the skills to load a specific skill if needed.

Hey Marco, thanks for this. Genuinely useful, and appreciate the disclosure up front about how the review was done. It’s a fair read of the public repo.

Two things from the live side that the repo doesn’t show. The device-code flow is real, it just lives entirely in the proxy right now (the /auth/device/* routes), so you’re right that it’s exactly the kind of logic an on-chain migration has to pull in. And the verifier gate is already live: the proxy signs with its own identity and the canister rejects social logins from any other principal. So the canister already knows the proxy’s principal, which is the lever for most of the write-path authorization work

Order I’m taking it in: write-path auth and credential entropy first (those are cheap and unblock everything else), then hashing the email join key so plaintext stops living in canister state, then the persistence migration to mo:core collections so upgrades stop depending on the pre/postupgrade serialization, then pagination and certification on the read path. Direct exposure only after all of that, and I take the point that a thin stateless edge in front of writes stays even then. I’m not in a rush to point the domain at the canister until the canister doesn’t need the proxy to be safe.

The II angle for /link is a good one and I’ll come back to you on it once the groundwork above is done, since that’s the point where it becomes a real design choice

Noted on icp-cli and the skills. I’ll pick those up as part of the persistence work since that’s where the migrating-motoko-actors one applies.

Will post back here when the first batch is shipped

Chedz