← All projects
Sprint estimation · Realtime state + 4 integrations

Poker Architect

Realtime SaaS

Real-time planning poker for distributed teams: versioned room snapshots over Pusher, server-authoritative simultaneous reveal, and story points written back to Jira, GitHub, Trello and IzzyPlan.

Next.js 15TypeScriptPrismaPostgreSQLPusherNextAuthnext-intl
Role
Product and full-stack engineering
Timeline
2025 – 2026
Tech
Next.js 15, TypeScript, Prisma, PostgreSQL, Pusher, NextAuth, Tailwind, next-intl
Status
Live
State
Versioned snapshot
Integrations
Jira, GitHub, Trello, IzzyPlan
API
25 routes, OpenAPI
Product showcase

Flows & screens

01   Public landing in English and Portuguese, with direct link-based entry into a room.

How it's built

Implementation

01

Versioned snapshots instead of incremental events

The server owns room state and publishes the whole snapshot with a monotonic version number. Clients apply an update only when the incoming version is higher, so a duplicated, out-of-order or dropped event cannot corrupt the table. Pusher caps payloads at 10 KB, so large snapshots go out truncated and the client pulls the full state over GET.

02

Round-trips collapsed into one transaction

Routes that ran up to seven serial database round-trips now send their writes along with the snapshot read inside a single prisma.$transaction. The Pusher broadcast moved into Next's after(), off the response path, and a failed broadcast never fails the mutation because the reconciliation poll covers it.

03

Simultaneous reveal and races settled in the database

Reveal uses an absolute server instant: the client corrects its own clock offset and the server clamps the proposed value so nobody can rush or delay everyone else's countdown. Two concurrent reveal clicks become an updateMany with the condition in the filter, and a vote is an upsert on the participant + story composite key.

Overview

Poker Architect is a planning poker tool for teams that estimate remotely. Rooms open from a link, votes stay hidden until reveal, and the number lands before the discussion, which is the whole point for teams using the ritual to avoid anchoring on the loudest senior in the call.

The app is Next.js 15 on the App Router with TypeScript, Prisma 6 over PostgreSQL and NextAuth v4 with database sessions and GitHub or Google sign-in. The interface is Tailwind with shadcn/ui on Radix, light and dark themes, and next-intl covering English and Portuguese across every screen. It runs 9 pages, 25 API routes and 14 Prisma models.

Realtime is Pusher Channels, one channel per room and a single event, room:state. The interesting part is not the transport, it is the state model around it.

The state problem

A planning poker table has several people mutating the same object at once, on different networks, some behind corporate firewalls that block WebSockets. Emitting incremental events like "Ana voted" breaks the moment one event is lost: from then on, every client renders a different table.

Here the server is the source of truth and publishes the entire room snapshot with a monotonic version. The client compares versions and discards anything stale. As a result:

  • A dropped event fixes itself on the next snapshot.
  • Network reordering has no effect.
  • Anyone joining mid-round receives the full state at once.

On top of that sits the safety net: adaptive reconciliation polling, every 20 seconds while the socket is connected and every 4 seconds when it drops, running only while the tab is visible and reconciling on focus, online and visibility changes. Presence uses a 45-second heartbeat plus a sendBeacon on pagehide, because leaving a room cannot depend on a request finishing.

Latency

The database is remote, so every round-trip is expensive. Routes that serialized four to seven queries were pushing past a second. The fix was to send the writes together with the snapshot read in the same transaction, and to bump the version with update ... returning instead of spending a query reading it back. The broadcast left the critical path through Next's after().

Round privacy

The broadcast snapshot carries only hasVoted. Vote values appear after the story is revealed, and a participant's own vote comes back in the response to their own GET. You cannot open devtools and read a teammate's card early.

Integrations

TrackerAuthPulls tasksWrites points
JiraOAuth 3LO with cloudId discoveryJQL for unfinished itemsStory points field, comment fallback
GitHub IssuesOAuth with repo scopeOpen issues, PRs filtered outComment on the issue
TrelloFragment token with a callback bridgeBoard cardsComment on the card
IzzyPlanAPI token validated before savingProjects and tasksNo, the public API is read-only

A provider with missing environment variables shows as unavailable instead of offering a button that breaks.

What it does not have

No automated tests and no rate limiting so far. Architecture notes live in CLAUDE.md and in long comments at the concurrency-sensitive spots, which is what held the decisions above together while the product kept changing.