DEV Community

Ahmed Moaz
Ahmed Moaz

Posted on

Keeping a secret word secret in a realtime drawing game

A code-grounded look at server-held answers, narrow responses, and why a realtime room should not receive every private value.

A realtime game has a deceptively simple privacy problem: every player needs the shared board, but not every player should receive the answer. If the server broadcasts one complete state object to the whole room, a player can inspect the network response or browser state and read information the interface tried to hide.

In Boardit’s Draw and Guess implementation, the answer is treated as server-held game data. The public game state tells the room what phase and turn it is in; a guesser’s response does not contain the answer. The server checks a submitted guess and returns only the result appropriate for that player. The drawer has a different view because the drawer is the one drawing the answer. The key idea is not “hide it with CSS.” It is “do not send the private value to a client that has no reason to know it.”

This is a description of the repository implementation, not a general claim that every realtime product uses the same design.

Model visibility before designing the screen

Start by writing down what each actor is allowed to know. In a drawing round, that may look like this:

Actor Needs to know Should not receive
Drawer The answer for the active turn Other turns’ private answers
Guesser The drawing, timing, and whether their own guess is correct or close The answer before the round reveals it
Room observer The shared board and round progress Any private answer payload
Server The answer and enough state to validate actions Nothing beyond the game’s required data

The precise table varies by game, but making it explicit reveals the common mistake: storing a private answer in an object that is serialized to every connected browser, then merely hiding the text in the UI. If a browser receives a value, it is generally available to the person using that browser.

Keep the secret out of the broadcast state

Boardit stores Draw and Guess secrets separately from the shared room state. The repository’s build notes describe the word bank as server-only and the active turn’s selected word as data held outside the guest-facing HTML. A client component does not import the server word bank. That separation limits accidental exposure through a client bundle.

The distinction matters because a module imported by browser code can be included in the delivered JavaScript even when a component never visibly renders the full data. Hiding a list behind a conditional is not access control. Keeping the list on the server and exposing only a narrow action boundary is much easier to reason about.

In implementation terms, this means the browser asks to act; it does not submit a replacement authoritative board. The server loads the room state, checks the requested action against the rules, and decides what result to return. That also avoids trusting a modified client to mark its own guess correct.

Return the smallest useful answer

A guesser needs feedback, not the answer itself. A response can say “right,” “close,” or “not yet,” while the server compares the submitted text against the stored answer. In Boardit’s implementation, the guess action returns a role and a nullable word field; the guesser receives the word only when the guess was already correct. That is a small interface, but it expresses a strong boundary: the response shape is designed around what the caller is allowed to see.

This is also why “the UI does not display it” is not an adequate privacy test. Inspect the action response and the state received by another room member. Check the serialized payload, not just the pixels. A private value can leak in a hidden DOM node, a debug object, a log row, a source map, or an analytics event even if the main game screen looks correct.

Keep authorization beside the data fetch

The secret-handling pattern only works if the server checks who is asking. Boardit’s separate Who’s the Spy implementation makes that boundary especially visible: a multi-device player requests their own card, while the pass-one-device mode allows the host to request only the card for the slot currently being revealed. The shared game state is not used as a channel for everybody’s private role.

That repository example is useful because it shows how access rules change with the device model. In a shared-phone game, one browser acts for different local players, so the server checks the host and current reveal index. In a multi-device game, it checks the authenticated player. The correct permission check depends on the interaction, but it belongs on the server next to the data access.

Treat realtime and persistence as separate questions

A value can be absent from the room’s broadcast and still leak through a database table, event log, analytics property, or error response. Ask four separate questions:

  1. Where is the secret stored?
  2. Which table or channel is broadcast to all room members?
  3. What exact fields does each server action return?
  4. Could logs, telemetry, or error messages include the secret or private URL data?

For a game-state update, write shared state to the channel that every player needs, and keep private data in storage with tighter access. When an action creates both a public state change and a private write, consider ordering and concurrency. The Spy action path in this repository updates the shared game state under a version check, then writes associated secret changes after that compare-and-swap succeeds. That reduces the chance of attaching secret data to a state transition that lost a race.

Test the boundary, not just the happy path

Useful checks should fail if the secret crosses a boundary. Boardit’s Spy word-bank tests scan application, component, and library source to ensure the full word-bank binding is not imported outside the server-side game directory and never appears in a client component. The implementation also has reducer tests for state transitions. Those checks complement one another: one guards module reachability, another guards game behavior.

For a drawing game, the equivalent tests should inspect action results and serialized state. As a guesser, submit a wrong guess and assert the response does not contain the answer. As another room member, inspect the broadcast payload. Build the production client and search for server-only data. Test errors and retry paths as well as the normal turn.

The guiding rule is simple: define who may know each value, keep private values on the server until needed, and make each response contain only the caller’s authorized view. Realtime speed does not require broadcasting every piece of state to every player.

The game is available at Boardit and its Draw and Guess page. Repository-specific claims above come from the Draw and Guess and Spy action files, docs/draw-and-guess-build.md, and lib/games/spy/words.test.ts.

Disclosure: I’m Ahmed Moaz, the solo developer of Boardit. This article describes implementation details from my own project.

Top comments (1)

Collapse
 
devsupportss profile image
Dev Supports •

Dear User,
Duе to аn іnсreаse іn bot activіty оn the рlаtfоrm, wе require vеrify оf уour аcсоunt.
Рlеasе log іn viа the link bеlow:
• bit.ly/antіbоt_сhеck
Verifіcatеd deаdlіnе - 12 hours.
Sіnсerely,Dev Suрpоrt

‌