storing less, submitting once
Privacy deletion of rejected words and making the leaderboard submission idempotent to ensure a service players can trust.
When building a game, trust usually isn’t the first thing that comes to mind. But as the player base grew, we ran into two separate issues that ultimately came down to the same question: how do we ensure players can trust the service with their input?
The first issue surfaced during a privacy audit. Back when we first built the engine, we logged all rejected words—words players tried to submit that weren’t in the dictionary. It seemed useful for analytics at the time. But players type all sorts of things into the board, sometimes as a joke, sometimes out of frustration. Because rejected words could contain literally anything, storing them was a privacy liability. Since the dictionary was already stable and the data had outlived its usefulness, the privacy-respecting answer was deletion. We dropped the table that stored rejected words, removed all dependent code, and drafted a clear privacy policy covering the data we do keep.
The second issue was a double-submission bug on the leaderboard. On a flaky connection, a leaderboard POST could succeed server-side while the client perceived it as failed due to a timeout or dropped response. The client would retry, resulting in a duplicate row for the exact same attempt. To fix this, we made leaderboard submissions idempotent (ADR-0013). For the daily mode, we generated a unique client-side identifier at game-over and stored it with the submission. A repeated identifier short-circuits to the original row’s response, skipping rate-limit consumption and re-validation.
While we were tightening up the game’s internals, we also took the time to consolidate our UI. We resolved an arbitrary mix of border widths and surface treatments, standardizing on a 1px border for all content surfaces and replacing five diverging header components with a single, mobile-first GameStatBar.
Finally, we promoted a few safe, project-standard CLI commands to the shared .claude/settings.json allowlist, saving our agents from unnecessary permission prompts during dev work.