A leaderboard that is hard to cheat

18 October 2026 · 6 min read

Any game where the score is computed on the player's device has a cheating problem, and pretending otherwise is dishonest. If the code runs on a phone, the phone's owner can change it. The realistic goal is not "impossible to cheat" but "the obvious cheats do not work, and the effort required is higher than the reward".

Here is what the Candy Mass leaderboard does, in order of how much it matters.

Rule one: you can only write your own row

Every player's score lives in a document keyed by their authenticated user ID. The security rule enforces that a write is only allowed if the authenticated user matches the document ID:

match /daily/{date}/scores/{uid} {
  allow read: if true;
  allow write: if request.auth != null
               && request.auth.uid == uid;
}

This is the single most important rule. Without it, one player could overwrite every other player's score. With it, the database refuses the write regardless of what any client sends. A modified app cannot get around this because the check happens on Google's servers, not in the app.

Rule two: scores only go up

The second rule compares the incoming value with the stored one and rejects any write that lowers it:

allow update: if request.auth.uid == uid
  && request.resource.data.score
     >= resource.data.score;

This blocks a specific and common cheat: reinstall the app, which resets the local score to zero, and the first submission wipes the real score. It also means a genuine player who clears their data cannot accidentally erase their record.

Rule three: the client reads before it writes

This one is not a security rule, it is a client behaviour, and it exists because of a bug.

With rule two in place, a player who cleared local data would try to write zero, the server would reject it, and the console filled with permission errors. Nothing was actually broken, but it looked broken.

The fix was to make the client read the cloud value first, compare, and skip the write entirely if the cloud value was already higher:

const snap = await getDoc(ref);
const cloud = snap.exists() ? snap.data().score : 0;
if (localScore <= cloud) return;   // nothing to do
await setDoc(ref, { score: localScore }, { merge: true });

Now the client only writes when the write will succeed, and the errors are gone. The general lesson: when a server-side rule rejects a write, the answer is almost never to weaken the rule. It is to stop making a write you did not need.

What this does not stop

Being honest about the limits matters.

A player who modifies the client can submit an arbitrary high score for their own account. Rules cannot detect that, because the number itself is not obviously wrong.

Blocking that requires either server-authoritative gameplay — the server simulates the round, which means a real backend and a real budget — or statistical detection, which means flagging scores that are implausible for the round reached.

Neither is in this build. The rules block impersonation, score erasure and bulk overwrites. They do not block a determined individual from lying about their own score. For a casual game with an optional leaderboard, that is a reasonable line.

Why bother at all

Because the cheap cheats are the ones that ruin a leaderboard. If anyone can overwrite anyone, the board fills with junk within a week and stops meaning anything. If anyone can erase their own score, players lose progress they care about.

Those two failure modes are blocked with about ten lines of rules. The remaining risk — a determined cheater inflating their own number — is expensive to fix and rare in practice.

Spend the effort where it changes the experience, and be honest about where it does not.

← Back to all articles

▶ Play Candy Mass
© 2026 Mass Interactive Games · massinteractivegames@gmail.com