The Problem 😩
Sprint planning. Your team needs to estimate tasks. Someone shares a Planning Poker link. You click. You wait. Half the team can't connect. The other half already went for coffee.
Sound familiar?
At AGG TEAM, we tried everything:
- Planning Poker Online (laggy, ads, ancient UI)
- Scrum Poker for Jira (expensive, requires Jira)
- PlanITpoker (works, but missing OUR features)
After the third crash mid-estimation, I thought: "Screw it, I'll build my own."
The Real Challenge: 6 Teams, 1 Room 🤯
Here's the twist: we don't have ONE scrum team. We have SIX departments:
- Frontend, Backend, DevOps, QA, Analytics, Management
Everyone wants to do planning poker. Simultaneously. In one room.
It's chaos. Like playing six different card games at the same table.
Existing tools say: "Here's one room, figure it out!" Not ideal.
What I Built
🎰 Multi-Table Support (Up to 6!)
Core concept: one session, multiple independent tables.
interface Table {
id: string;
name: string; // "Frontend Squad", "Backend Ninjas"
revealed: boolean; // Are cards revealed for this table?
}
interface Player {
id: string;
name: string;
tableId: string; // Which table they're at
vote: string | null; // "5", "13", "?", "☕"
}
Host creates tables with custom names. Everyone picks their table. Each table votes independently. You see all tables + an overall average.
Host powers: Add, remove, rename tables on the fly. Move people between tables if needed.
⚡ Real-Time Sync
Used Supabase as backend. Simple polling approach:
useEffect(() => {
if (view === 'room' && roomId) {
fetchRoomState();
const interval = setInterval(fetchRoomState, 2000);
return () => clearInterval(interval);
}
}, [view, roomId]);
Yeah, WebSockets would be cooler. But for prototypes and ~50 users? Polling works great. If it grows, I'll switch.
Updates in real-time:
- Someone joins → instant
- Someone votes → immediate
- Cards revealed → everyone sees results
- Emoji thrown → flies across screen
💓 Smart Disconnect Detection
Problem with other tools: people close tabs, but their avatars stay forever. Ghost users.
My solution uses heartbeat + explicit disconnect:
// Send heartbeat every 10 seconds
const sendHeartbeat = async () => {
await fetch(`${API_URL}/rooms/${roomId}/heartbeat`, {
method: 'POST',
body: JSON.stringify({ playerId }),
});
};
// Also explicit disconnect on page close
window.addEventListener('beforeunload', () => {
navigator.sendBeacon(
`${API_URL}/rooms/${roomId}/disconnect`,
JSON.stringify({ playerId })
);
});
Result:
- Page open but idle? Stay as long as you want (no kicks)
- Close page/tab? Instant disconnect (2 seconds)
No more ghosts. Clean rooms.
🎉 Emoji Attacks!
My favorite feature. Click any participant, throw an emoji. It literally flies from your avatar to theirs with smooth animation.
12 emojis: 👍 👏 🎉 ❤️ 🚀 🔥 😂 🤔 💯 ✨ 👌 🙌
Real use cases:
- 👍 agree with estimate
- 🔥 someone makes a great point
- 😂 junior estimates simple task at 89 points
- ☕ definitely coffee time
This made our plannings fun. People actually look forward to estimation meetings now!
🃏 Fibonacci + Special Cards
Classic: 0, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89
Plus:
- "?" → "I have no idea"
- "☕" → "Need coffee before thinking"
You can change your vote even after reveal. Why? Because during discussion you might realize 13 should actually be 8. Most tools lock votes. Mine doesn't.
🧮 Auto-Average Calculation
Each table shows its average. Plus grand total at bottom across ALL tables.
const calculateAverage = (players, revealed) => {
if (!revealed) return null;
const numericVotes = players
.map(p => p.vote)
.filter(v => v && !isNaN(Number(v)))
.map(Number);
if (numericVotes.length === 0) return null;
return (numericVotes.reduce((a, b) => a + b) / numericVotes.length).toFixed(1);
};
Perfect for: "What's the overall estimate across all teams?"
🌙 Dark Mode + 🌍 Multi-Language
Toggle in corner switches theme (light/dark) and language (EN/RU). Saved to localStorage, auto-applied next time.
Because if your app doesn't have dark mode in 2026... what are you even doing?
Tech Stack 🛠
Frontend:
- React + TypeScript
- Tailwind CSS v4
- Lucide icons
Backend:
- Supabase Edge Functions (Hono + Deno)
- Supabase KV Store (key-value table)
Why Supabase?
- Fast setup (no server deployment)
- Edge Functions (TypeScript backend)
- Simple KV store for room state
- Free tier for prototypes
Challenges & Solutions 💡
1. Ghost Users
Problem: Users close tabs, avatars stay forever
Solution: Heartbeat mechanism + explicit disconnect via navigator.sendBeacon
2. Z-Index Wars
Problem: Theme toggle covered modals
Solution: Proper z-index hierarchy (z-30 buttons, z-40 modals)
3. State Management
Problem: Keeping 6 tables + players in sync
Solution: Single source of truth in backend, polling for updates
How Planning Changed
Before:
- "Wait, is everyone loaded?"
- "Refresh, I don't see your vote"
- "Who's still here?"
- awkward waiting
After:
- Create room (5 seconds)
- Everyone joins (instant)
- Vote, reveal, discuss
- Throw emojis for fun
- Actually finish on time
Real feedback:
"Wait, planning poker can be smooth?"
"I love throwing fire emojis at people!"
"Finally a tool that doesn't make me rage-quit"
What's Next? 🚀
- Session history (who voted what)
- Voting timer (optional countdown)
- Custom decks (T-shirt sizes?)
- Sound effects (optional)
- Jira integration
Key Takeaways 🧠
1. Build what you need
Stop waiting for the "perfect" tool. 80% built in a weekend beats 100% coming "eventually."
2. Polling isn't evil
WebSockets are cool, but polling works great for small teams. Don't over-engineer.
3. Small delights matter
Emoji feature took 2 hours. It's everyone's favorite. Little things make big differences.
4. Dark mode is mandatory
Seriously. It's 2026.
Try It! 🎮
Use it, free
The Bottom Line
Sometimes a few evenings of coding saves months of frustration. Plus you learn something new. Win-win.
If your team has the same planning poker pain - build your own! We have amazing frameworks, free hosting, and AI assistants now. There's no excuse.
Questions? Ideas? Want to contribute? Drop a comment!
P.S. Yes, enterprise solutions exist. Mine cost $0, works perfectly for us, and was fun to build. 😄
Top comments (0)