I Built Yet Another Planning Poker App and I Don't Care
I forked a GitHub repo 7 years ago because I liked how the cards looked.
Redbooth's Scrum Poker Cards — a set of beautifully illustrated planning poker cards. Each card has an idiom matching the effort: "Piece of cake" for 2, "Here be dragons" for the unknown, "Monster task" for 100. They even had a platypus for 5, because ornitorinco rhymes with cinco in Spanish.
The repo was just SVGs and a README. I starred it, forked it, and forgot about it.
The idea that wouldn't die
Every few months I'd see the repo in my GitHub profile and think: "These cards should be a real app." Then I'd close the tab and do something else.
Last week I ran out of excuses. I wanted to:
- Play with WebSockets — specifically PartyKit, which runs on Cloudflare Workers
- Try PostHog — anonymous analytics without the Google Analytics guilt
- Deploy something on Cloudflare Pages — been using it for static sites, never for a real app
- Build with Claude Code — see how far AI pair programming goes on a greenfield project
- Ship something I'd actually use — my team does sprint planning every two weeks
Yes, there are 200 planning poker tools already. I don't care. I'm building for fun.
What I built
scrumpoker.work — a free, real-time planning poker app with the original Redbooth card illustrations.
The basics work:
- Create a session, get a shareable URL
- Anyone joins by entering their name — no accounts
- Pick a card, host reveals, see results with stats
- Sessions are ephemeral — nothing stored anywhere
The fun parts:
- When you select a card, the idiom floats up like you're calling it out: "Here be dragons!"
- Vote reveal shows contextual messages: "Ship it! 🚀" when everyone agrees, "We have absolutely no idea 🤯" when votes spread from 1 to 100
- Confidence toggle — vote 8 with "confident" vs vote 8 with "no idea" tells the team very different things
- Outlier detection — if you vote 13 when everyone else voted 3, the app asks "why?"
- Topic queue — host adds stories upfront, team votes through them in order
The tech stack
| What | Why |
|------|-----|
| React + TypeScript + Vite | Fast, typed, no framework overhead |
| Tailwind CSS + Framer Motion | Utility styles + playful animations |
| PartyKit | WebSocket rooms on Cloudflare Workers. One file (party/poker.ts) handles all real-time logic |
| Cloudflare Pages | Static hosting, edge-deployed, free |
| PostHog | Anonymous usage analytics, memory persistence (no cookies) |
| Sentry | Error tracking for when things break in production |
PartyKit is the interesting one
I've built WebSocket servers before — with Socket.io, with raw ws, even with Durable Objects directly. PartyKit abstracts all of it into something that feels like writing a React component.
One class. onConnect, onMessage, onClose. Room state lives in memory. Broadcasting is a for-loop. The whole server is one file, ~300 lines, handling:
- Player join/leave with stable identity (UUID in localStorage)
- Vote collection and reveal
- Host management and transfer
- Topic queue navigation
- Kick with confirmation
- Auto-reveal when all players vote
- 5-second grace period for reconnection (tab switch, reload)
Deploy is npx partykit deploy. That's it. It runs on Cloudflare's edge globally.
The reconnection problem
The hardest part wasn't the features — it was handling reconnections. When a user reloads their browser, their WebSocket connection dies and a new one opens. Every user has a connection ID that changes on reconnect. If you use the connection ID as identity (like I did initially), the server thinks it's a new player every time.
I went through 5 iterations:
- Name matching (fragile — what about duplicate names?)
- Grace periods with name lookup (race conditions)
- "Replaced" messages to old tabs (infinite reconnection loops)
- BroadcastChannel for tab coordination (helped, but didn't fix the root)
- Stable UUID in localStorage — the actual fix
The final approach: each browser gets a UUID stored in localStorage. The server maps stableId → player, not connectionId → player. Reconnects silently swap the socket. Other users see nothing. Tab switches use BroadcastChannel to coordinate which tab owns the connection.
Should have done this from the start. The lesson: your player identity should never be your transport identity.
Building with Claude Code
I built 90% of this with Claude Code in a single session. Not as a party trick — as genuine pair programming.
What worked well:
- Parallel agents — "create the server, the components, and the pages" in one message, three agents working simultaneously
- Iterating on UI — "the cards are cut by the container" → immediate fix with explanation
- Feature brainstorming — I pasted 10 feature ideas from a ChatGPT conversation, we evaluated each one together, picked 3 to build
- The boring stuff — SEO meta tags, schema markup, legal pages, robots.txt, sitemap — all generated correctly in parallel
What needed correction:
- First build had no card illustrations — the
Cardcomponent never loaded the SVGs - WebSocket host fallback used
??instead of||— empty string is truthy - Initial player identity used connection IDs (see reconnection saga above)
- Several
ascasts that violated my strict TypeScript rules
The pattern: Claude Code is great at generating the 80% and iterating on feedback. The 20% where it guesses wrong, you catch by testing. Having strict TypeScript and a tsc -b check after every change caught most issues before they reached the browser.
Numbers
- 26 commits in one session
- ~4,500 lines of code (including server)
- 14 illustrated cards from the original Redbooth set
- 5 card presets (All, Fibonacci, Simple, T-Shirt, Powers of 2)
- 0 databases — everything ephemeral
- 685KB JS bundle — needs code splitting, but works
- $0/month to run — Cloudflare Pages free tier + PartyKit hobby tier
What's next
The TODO list has ideas parked:
- Code splitting to reduce bundle size
- Dark mode
- Sound effects (optional)
- Silent poker (multi-round bias reduction)
- Import topics from Jira/Linear
- i18n (Portuguese, Spanish)
But honestly? It works. My team can use it next sprint. That's enough.
Try it
scrumpoker.work — create a session, share the link, estimate some stories.
Source code: github.com/letanure/Scrum-poker-cards
The card illustrations are CC BY 3.0 by Redbooth. Thank you for making them — 7 years later, they're finally in a real app.