Product Engineering · Real-Time Front-End
Planning Poker
A Connection Layer That Survives Bad Networks
I built a real-time planning poker app in TypeScript, end to end. The point is not the poker. It is a connection layer that survives bad networks, optimistic updates that roll back correctly, and tests that prove it.
Status: Completed
An independent portfolio project, built end to end in TypeScript. This case study is the write-up of the design and the hard problems.
The Problem
Planning poker looks simple: everyone picks a card, and nobody sees anyone's vote until someone reveals. Real-time apps fail in the gaps around that. A refresh in the middle of a round, a laptop that goes to sleep, a vote sent just as the round is revealed.
This project exists to handle those gaps on purpose, and to show the handling with tests instead of claims.
What It Does
Rooms are shared by link. Players join with a name, pick a card, and nobody sees anyone's vote until someone hits Reveal. Then a new round starts. Every player sees who is in the room, who has voted, and whether they are connected.
How It Works
One message flow covers everything:
Three packages live in one repo: a shared package of schemas that is the only place a message shape is defined, a Node server with in-memory rooms, and a Vite and React client with a small store. Everything is TypeScript.
What Done Meant
The bar was written at the top of the README on day one and not added to afterward:
- •Two browsers in the same room see each other's presence and votes within a second
- •Refreshing mid-round puts you back in the room with your vote intact
- •Going offline mid-round and coming back reconnects automatically, resyncs state, and re-sends an unsent vote
- •Voting after a reveal is rejected by the server and rolled back on the client with a visible reason
- •A player whose connection drops shows as disconnected after 3 seconds and is removed after 30, with no flicker on a quick refresh
- •The whole flow works keyboard-only, with the deck as a radio group and reveals announced to screen readers
- •Unit tests cover room rules, reconnect, and rollback, and browser tests drive two players through join, vote, reveal, and an offline cycle
Design Decisions
The client id is the identity
Sockets come and go. A browser-generated id stored locally lets the server treat a refresh or reconnect as the same player.
Votes are hidden on the server
Hiding votes only on the client would be defeated from DevTools, so the server keeps them private until reveal.
Every vote has a message id
The client shows a vote instantly and matches the server's answer to that exact attempt, which is what makes the optimistic update correct.
Snapshots, not diffs
Rooms are small, so a full snapshot is a few kilobytes and makes reconnect trivial. Deltas would be the answer at larger scale.
Grace timers for presence
A player shows as disconnected after 3 seconds and is removed after 30. Both timers cancel on rejoin, so a quick refresh never flickers.
One home for the optimistic value
The unconfirmed vote lives in a single field and is never written into the room state, so rollback is just clearing that field.
Accessibility
The interaction is the product, so accessibility is part of the build, not a pass at the end:
- •The deck is a real radio group: arrow keys move between cards, and Enter or Space votes
- •On reveal, focus moves to the results heading so keyboard and screen-reader users land on the outcome
- •Reveals and new rounds are announced through a live region, rate-limited so a busy room stays usable
- •Presence and vote state are text, never color alone
How It Is Tested
Three layers, each testing something the others cannot:
- •Room rules on the server with fake timers and no sockets, including the 3 and 30 second presence windows
- •The socket layer in the browser against a fake WebSocket, including backoff and the dead-connection ping
- •Two real browsers against the real server, including a vote placed while offline and delivered after reconnect
Claims stay proportional to what was tested, and the README lists what was not.
Scope and Limits
Rooms live in memory and disappear when the server restarts. At scale they would move to a shared store with pub/sub between instances, which is not built here on purpose.
Anyone in a room can reveal or reset, as in real planning poker, which keeps the project out of authentication. The server needs a host that keeps long-lived connections open, which rules out serverless functions.
