PrimoDev/ apps

Pokerface

Planning poker em tempo real, simples e self-hosted

  • Web
  • Self-hosted
  • Grátis, open source (MIT)
  • Open source · MIT

O que é o Pokerface?

O Pokerface é um app de planning poker pra times estimarem histórias juntos em tempo real. Você cola os links das tarefas, convida o time pela URL da sala e todo mundo vota com as cartas viradas até o facilitador revelar.

Dá pra usar grátis em poker.oprimo.dev ou hospedar no seu servidor: é open source (MIT) e roda num único processo Node com SQLite, inclusive via Docker.

O que ele faz

  1. 01Votos, revelação e reset em tempo real via WebSocket
  2. 02Cartas escondidas até o facilitador revelar, com média e distribuição dos votos
  3. 03Rodadas a partir de links do Jira, Linear ou GitHub (ou linhas de texto)
  4. 04Baralhos Fibonacci e T-shirt, além de baralhos personalizados
  5. 05Timer de votação de 60, 30, 15 ou 5 segundos
  6. 06Sessões anônimas de 24 horas ou sessões salvas com conta e histórico
  7. 07Exportação das rodadas e votos em CSV ou resumo em texto
  8. 08Self-host num único processo Node com SQLite

Perguntas frequentes

O Pokerface é grátis?
Sim. Ele é open source sob licença MIT e dá pra usar a instância em poker.oprimo.dev ou hospedar a sua.
Preciso criar conta pra usar?
Não. No modo anônimo você escolhe um nome e compartilha a URL da sala, que dura 24 horas. A conta (email e senha) serve pra salvar sessões e o histórico de votos.
Dá pra importar tarefas do Jira ou do Linear?
Dá pra colar URLs do Jira, Linear ou GitHub, ou linhas de texto simples. Cada uma vira uma rodada de votação.
Quais baralhos ele tem?
Fibonacci (0, 1, 2, 3, 5, 8, 13, 21, 34, ? e café) e T-shirt (XS a XXL, ? e café), além de baralhos personalizados.
Como hospedo o Pokerface no meu servidor?
Com Node 18+ e pnpm, rode pnpm install, pnpm build e pnpm start. Ele sobe na porta 3000 e cria um banco SQLite. O README também traz um Dockerfile e as variáveis de ambiente.
Dá pra restringir quem cria conta?
Dá. A variável ALLOWED_SIGNUP_DOMAINS limita o cadastro aos domínios de email da sua empresa, e ALLOWED_ORIGINS restringe as origens aceitas pela API e pelo WebSocket.

Guias

Documentação

README do repositório lucianodiisouza/pokerface, atualizado a cada deploy. O texto original é em inglês.

A self-hostable, real-time planning poker app for team estimation. Paste your task links, invite the team, and vote on stories together over WebSockets. Works anonymously with no sign-up, or create an account to save sessions and keep history.

Inspired by planningpokeronline.com, kept small and hackable.

Features

  • Real-time — everyone sees votes, reveals and resets instantly over WebSockets.
  • Hidden votes — cards stay face-down until the facilitator reveals.
  • Results — average and vote distribution, with a consensus nudge.
  • Rounds from links — paste Jira / Linear / GitHub URLs (or plain lines) and each becomes a round; work through the backlog one story at a time. Add more mid-session, skip, re-activate, or set the agreed estimate when a round finishes.
  • Decks — Fibonacci and T-shirt presets, plus fully custom decks. Signed-in users get a saved default deck applied to new sessions.
  • Voting timer — the facilitator can start a 60/30/15/5s countdown per round.
  • Facilitator hand-off — the facilitator can pass the role to any participant. In an anonymous session it transfers; in an owned session the owner stays in control and the target becomes a co-facilitator (so a teammate can run the meeting).
  • Export — download any session's finished rounds and votes as CSV or a plain-text summary, from the session-complete screen or the rounds sidebar.
  • Two session modes:
    • Anonymous — pick a name, share the room URL. The session runs for 24 h, then self-destructs. The first person in runs it, and the role passes on if they leave.
    • Owned (signed in) — the session and its full vote history are saved to your account and persist indefinitely until you delete them. Only you facilitate it.
  • Accounts — optional email + password sign-in (bcrypt-hashed, JWT bearer tokens).
  • Persistent — accounts, saved sessions, rounds and vote history live in SQLite.
  • Single process — one Node server serves the API, the WebSocket, and the web app.
  • Lightweight monorepo — three small TypeScript packages, no heavy framework.

Architecture

packages/
  shared/   TypeScript types for the WebSocket protocol + REST shapes (used by both sides)
  server/   Node + ws + better-sqlite3 — REST API, live rooms, and serves the built web app
  web/      Vite + React + TypeScript client (React Router, React Query, react-hook-form)

Live room state (participants, current votes, reveal state, timer) is held in memory for speed; rooms, rounds, recorded votes and accounts are persisted to SQLite so they survive restarts. A live room is lazily loaded from the DB on first join and dropped from memory when it empties.

The web client talks to the backend over REST (/api/*) for auth and room setup, and over a WebSocket (/ws) for the live session. In dev, Vite proxies both to the server; in the all-in-one production build the server serves the built client alongside the socket.

Quick start

Requires Node 18+ and pnpm.

pnpm install
pnpm build
pnpm start

Then open http://localhost:3000, create a room, and share the URL with your team. The server creates a SQLite database at ./data/pokerface.db on first run.

Set a custom port with PORT:

PORT=8080 pnpm start

Configuration

All configuration is via environment variables on the server:

Variable Default Purpose
PORT 3000 HTTP + WebSocket port.
DATABASE_PATH ./data/pokerface.db SQLite file location. Point at a persistent volume in production.
JWT_SECRET (random per boot) Signs session tokens. Set a stable value in production (see below).
ALLOWED_ORIGINS (unset = any) Comma-separated origin allowlist for the API (CORS) and WebSocket.
ALLOWED_SIGNUP_DOMAINS (unset = any) Comma-separated email-domain allowlist for registration (e.g. acme.com).

The web client has one build-time variable, VITE_WS_URL, used only for the split Railway + Vercel deploy (see below). Unset, the client talks to the same origin at /ws.

Important

If JWT_SECRET is unset, the server generates a new random secret on every boot, so every account is logged out on each restart/redeploy. If DATABASE_PATH points at a container's ephemeral disk (the default on most PaaS), accounts, saved sessions and history are wiped on every redeploy — mount a persistent volume and point DATABASE_PATH at it.

Development

Runs the server (:3000) and the Vite dev server (:5173) with hot reload:

pnpm dev

Open http://localhost:5173 during development.

Deployment (split: Railway + Vercel)

The reference deployment runs the backend on Railway and the frontend on Vercel at https://poker.oprimo.dev.

  • Backend (Railway) — deploys from railway.json. Railway builds with Nixpacks, starts @pokerface/server, and health-checks /healthz. Railway assigns a public domain like pokerface-production.up.railway.app.

  • Frontend (Vercel) — deploys from vercel.json: builds only @pokerface/web and serves packages/web/dist with an SPA rewrite. Set the build-time env var so the client knows where the backend lives:

    VITE_WS_URL = wss://<your-railway-domain>/ws
    

When VITE_WS_URL is unset the client falls back to the same origin, which is what the all-in-one self-host setup below relies on.

Continuous deployment

Both services auto-deploy on every push to main:

  • Railway is connected to this GitHub repo (service source → main) and rebuilds the backend on push. It needs a Volume (mounted at /data) plus the env vars DATABASE_PATH=/data/pokerface.db and JWT_SECRET so accounts and history persist. For a split deployment, also set ALLOWED_ORIGINS to your frontend origin (e.g. https://poker.oprimo.dev) so the API and WebSocket only accept requests from it.
  • Vercel is connected to the same repo and rebuilds the frontend on push, using the VITE_WS_URL project env var to reach the backend.

Self-hosting (all-in-one)

It's a single stateless Node process — deploy it anywhere that runs Node.

Docker:

FROM node:22-alpine
WORKDIR /app
RUN corepack enable
COPY . .
RUN pnpm install --frozen-lockfile && pnpm build
EXPOSE 3000
CMD ["pnpm", "start"]

Behind a reverse proxy (nginx, Caddy, Traefik), make sure WebSocket upgrades on /ws are forwarded. For a single instance no sticky sessions are needed; if you scale horizontally you'd need a shared room store (out of scope for this simple app).

Security

Defense is layered across the request lifecycle:

  • Origin allowlist — set ALLOWED_ORIGINS (comma-separated) to restrict both the REST API (CORS) and the WebSocket (anti-CSWSH) to those origins. Unset, any origin is accepted (convenient for the all-in-one self-host, where the app is same-origin anyway).
  • Rate limiting — register/login (20 / 15 min) and room creation (40 / hour) are capped per IP; each WebSocket connection is capped at 25 messages/second. Behind a proxy the client IP is read from X-Forwarded-For.
  • Message limits — WebSocket frames are capped at 64 KB and HTTP bodies at 1 MB.
  • Security headers — the self-hosted server sends a strict Content-Security-Policy plus X-Frame-Options: DENY (clickjacking), X-Content-Type-Options: nosniff, Referrer-Policy and Permissions-Policy. The Vercel frontend sets the same via vercel.json.
  • Session secret — always set JWT_SECRET in production so tokens survive restarts.
  • Input guards — names, decks and pasted task text are length-clamped, and votes are validated against the active deck before they're counted.
  • By construction — all SQL is parameterized (no injection), auth is a Bearer token in a header rather than a cookie (no CSRF), passwords are bcrypt-hashed, and task URLs are restricted to http(s) so they can't become javascript: links.

Residual risk to be aware of: a room's share link is a capability URL — anyone with it can join the session and see round titles, statuses and agreed estimates. Room IDs are unguessable 10-char tokens, but treat the link like a secret. Per-person vote history is public for anonymous rooms and private to the signed-in owner for owned rooms. No user email or password data is ever exposed through the room API.

There is no email verification or password reset yet. Registration is open by default; set ALLOWED_SIGNUP_DOMAINS to your company's email domain(s) to limit who can create an account, and/or put the deployment behind your VPN/SSO.

Customizing the deck

Edit DEFAULT_DECK in packages/shared/src/index.ts.

License

MIT — do whatever you like.