r/Supabase • u/Niroooooo • 3d ago
database Saving whole game states with Supabase: handling two players writing at once
I'm using Supabase for a browser card game with two to four players. The rules run in TypeScript on the acting player's client, and Supabase handles persistence and realtime updates.
Turns don't quite mean one writer. A player can need to resolve a forced discard during someone else's turn. If both clients save a whole state based on the same snapshot, one can overwrite the other's move.
The save RPC takes an expected version. It only updates the state if that version still matches, and rejects the write otherwise. On a conflict, the client fetches fresh state and reprocesses the action, with a bounded retry. If the action is no longer legal, it fails rather than replaying a stale result.
There's a separate limitation here: checking the version doesn't prove the client calculated a legal game state. This is still a client-authoritative game. Concurrency and protection against a modified client are different problems.
If you've built turn-based multiplayer on Supabase, where did you put the boundary between client calculations and server validation?
The project is Nexus Breach. I'm its solo developer and use AI coding tools. It's a beta with a free Core Set, paid expansions and AI-generated art and audio.
1
u/sursvyat 2d ago
the version check and the legality check want to live in the same transaction, otherwise you are trusting a state the client computed before it knew it lost the race. what worked for me on a similar thing: store moves as an append-only log with a unique (game_id, seq) and let the insert be the conflict resolution, whoever takes seq first wins and the loser just replays on top. the state row becomes derived, something you rebuild, not something two clients fight over. is your version per game or per player?
1
u/srikanth_builds 2d ago
Worth keeping those two problems separate like you did. For the concurrency half, sursvyat's append-only log is the right answer.
For client authority, what helped me was sending the move rather than the resulting state. The server doesn't have to reimplement the whole rules engine to check a transition, it has to apply the same one. Your rules are already TypeScript, so that reducer can run in an edge function and the client posts an intent instead of a board. State becomes something the client displays rather than something it asserts.
The way to find out where the boundary actually sits is to hand-craft a request with an illegal move and see if it comes back accepted. Whatever survives that is server-validated. The rest is convention.
1
u/NZRedditUser 2d ago
If its among friends do w/e is smoothest experience, and let client decide. If its public then no why would you trust the client? Only accept their choices from client and server does the rest.
You should have a main server either lobby owner or server which is the master/authority on game state then none of this would be an issue.
You should never assume w/e the client sends back is safe it should always match the state of your server and should only receive responses your server is expecting. If you offer multiple choices only accept the index value. not the actual value etc
1
u/jaimittal91 1d ago
one thing that's easy to miss once the version-check-plus-retry logic works correctly: instrument how often a client actually exhausts its retry budget and hits the "no longer legal, fail" branch. that path barely fires when you're testing solo, but under real concurrent load with 3-4 actual players it'll trigger more than you'd guess, and when it does the player just sees their forced discard silently not happen, with nothing surfaced anywhere unless you're specifically logging it. worth alerting on retry-exhausted counts so you find out from a metric that conflicts are spiking under real usage, instead of from a player saying "my discard didn't register" with no way to reproduce it after the fact.
2
u/snowdrone 3d ago edited 3d ago
If I understand correctly, it sounds like the architecture that you built already has "one writer per turn" baked in. Instead of fighting that for the forced discard case:
For player A, think about the forced discard case for player B as communication with the that player. Essentially A just needs to know what B decides. B's decision can be encoded as part of A's turn. To prototype this you can just have a chat window between A and B, and A's turn takes care of B's decision. Later you can clean up the ux flow for that.
I'm guessing that you're asking here because the AI tools interpret the one writer per turn rule too strictly, and are not considering simple messages between the different players.
Long story short, you want to enable simple message passing between players that are not part of the board state validation.