██╗ ██╗ █████╗ ███████╗██████╗ ╚██╗ ██╔╝██╔══██╗██╔════╝██╔══██╗ ╚████╔╝ ███████║███████╗██████╔╝ ╚██╔╝ ██╔══██║╚════██║██╔═══╝ ██║ ██║ ██║███████║██║ ╚═╝ ╚═╝ ╚═╝╚══════╝╚═╝
Lightweight · Realtime · Self-hosted · Ephemeral by design
🌐 app.yasp.team · 🐳 wleonhardt/yasp
Planning poker should feel like a team ritual, not infrastructure management.
YASP is a fast, no-fuss collaborative estimation tool. No accounts. No stored history. Show up, estimate together, leave. The work lives in your tracker, not here.
┌─────────────────────────────────────────────────────────────┐
│ │
│ 🧑💻 Just want to use it? → app.yasp.team │
│ │
│ 🐳 Self-host it? → Quick Start below │
│ │
│ 🏗️ Run it in production? → Deployment section │
│ │
│ 🛠️ Hack on it? → Local Development │
│ │
│ 🌍 Improve a translation? → Contributing guide │
│ │
└─────────────────────────────────────────────────────────────┘
You ─── create/join room ──► Server ◄── teammates join ─── Team
│
Server owns
all room state
│
You pick a card ◄──────── broadcasts ──────────► teammates see
(hidden until reveal) updates (their cards too)
│
Moderator hits Reveal ──► all votes shown ──► stats: avg · median · mode
│
Next round or call it done
No round data persists after reset. Export before you move on if you need a record.
| Feature | |
|---|---|
| ⚡ | Realtime voting via WebSockets |
| 🃏 | Multiple deck presets + custom decks |
| 👀 | Spectator mode |
| 🔄 | Reconnect-friendly — rejoin mid-session |
| ⏱️ | Shared round timer with presets, pause, auto-reveal |
| 🎯 | Reveal / reset / next round flows |
| 📊 | Results with avg, median, mode, spread, consensus |
| 🔁 | Moderator transfer + disconnect handoff |
| 📋 | Round reports with CSV / JSON / Print export |
| 🌍 | Localized in 9 languages |
| 🦾 | Keyboard-navigable, live-region announcements |
| 🧼 | No database · No Redis · No external services needed |
docker run --rm -p 3001:3001 wleonhardt/yasp:mainOpen → http://localhost:3001
Three things true once this command runs:
- a full scrum poker app is live
- nothing was installed on your machine
- nothing will remain when you stop it
┌──────────────────────────────────────────────────────────┐
│ No accounts No stored history │
│ No database No persistence layer │
│ No migrations No infrastructure sprawl │
│ No stale rooms No baggage │
└──────────────────────────────────────────────────────────┘
All state lives in memory. Rooms exist for the meeting you're in right now. When the container restarts, rooms clear — and that's intentional.
YASP is not a planning system of record. It's the room you walk into, estimate, and walk out of.
Redis mode (opt-in) doesn't change this philosophy. It stores TTL-bound active state across process restarts — not history, not audit logs. Single-instance only. See docs/horizontal-scaling.md.
One-off session — gone on Ctrl-C:
docker run --rm -p 3001:3001 wleonhardt/yasp:mainPersistent background service — survives reboots:
docker run -d --restart unless-stopped --name yasp -p 3001:3001 wleonhardt/yasp:mainBuild locally:
docker build -t yasp:local .
docker run --rm -p 3001:3001 yasp:localApple Silicon: add --platform linux/amd64 if you need the x86_64 image target.
┌────────────────────────────────────────────────────────────┐
│ Browser │
│ React 18 + Vite SPA (port 5173/dev) │
└────────────────────────┬───────────────────────────────────┘
│ HTTP + Socket.IO
┌────────────────────────▼───────────────────────────────────┐
│ Fastify + Socket.IO (port 3001) │
│ │
│ Server is authoritative. Clients emit commands: │
│ cast_vote · reveal_votes · timer actions · etc. │
│ Server validates, updates state, broadcasts back. │
└────────────────────────┬───────────────────────────────────┘
│ optional (YASP_STATE_BACKEND=redis)
┌────────────────────────▼───────────────────────────────────┐
│ Redis (TTL-bound active state) │
│ single-instance · no history │
└────────────────────────────────────────────────────────────┘
| Layer | Technology |
|---|---|
| Client | React 18 + Vite |
| Server | Fastify 5 + Socket.IO 4 |
| Shared contracts | TypeScript project refs (shared/) |
| Runtime | Node.js 20+ |
| Default deploy | Single Docker container |
| Production deploy | OCI Always Free (docs/oci-always-free.md) |
| Optional infra | Manual AWS CDK (cdk/) |
sessionId is a browser continuity token in localStorage. It powers reconnect and latest-tab-wins. It is not an account or identity proof.
yasp/
├── client/ React + Vite SPA
├── server/ Fastify + Socket.IO runtime and tests
├── shared/ Shared TypeScript types and event contracts
├── cdk/ Manual optional AWS deployment stack
├── docs/ Deep-dive operational and contributor docs
├── plans/ ADRs, work queue, open questions
└── tests/ Script-level and Playwright checks
Prerequisites: Node.js 20+, npm 9+
git clone https://github.com/wleonhardt/YASP.git yasp
cd yasp
npm install
npm run devStarts two processes:
http://localhost:3001 ← Fastify + Socket.IO server
http://localhost:5173 ← Vite dev client (hot reload)
| Command | Purpose |
|---|---|
npm run dev |
Client + server in watch mode |
npm test |
Script tests + server Vitest + client Vitest |
npm run test:a11y |
Playwright accessibility smoke suite |
npm run i18n:check |
Validate locale key parity and placeholders |
npm run lint |
ESLint, zero warnings |
npm run lint:strict |
Type-aware rules (advisory) |
npm run build |
Production build (shared → server → client) |
npm run format:check |
Prettier verification |
npm run knip |
Unused files/exports/deps |
No .env file required for the default memory profile.
| Variable | Default | Purpose |
|---|---|---|
PORT |
3001 |
HTTP + WebSocket listen port |
HOST |
0.0.0.0 |
Bind address |
YASP_STATE_BACKEND |
memory |
memory or redis |
REDIS_URL |
— | Required when backend is redis |
NODE_ENV |
unset locally | Set to production in Docker/prod |
| Profile | Status | What it does | What it doesn't do |
|---|---|---|---|
memory |
✅ default | Active rooms in-process | History · multi-instance |
redis |
⚙️ opt-in | Active state with TTL, survives restarts | History · true horizontal scale |
redis mode is still single-instance. Multiple nodes pointed at the same Redis remain out of scope until cross-node fanout, timer ownership, and write coordination are solved. See docs/horizontal-scaling.md.
Published tags:
wleonhardt/yasp:main Rolling build from main branch
wleonhardt/yasp:<short-sha> Immutable commit-pinned tag for rollback/debug
The image runs hardened by default — non-root user, read-only filesystem, dropped capabilities:
docker run --rm \
--read-only --tmpfs /tmp:size=64m \
--cap-drop ALL --memory 512m \
-p 3001:3001 wleonhardt/yasp:mainGET /api/health → { "ok": true }
# Docker Compose healthcheck
healthcheck:
test: ["CMD", "curl", "-sf", "http://localhost:3001/api/health"]
interval: 30s
timeout: 5s
retries: 3The image ships a HEALTHCHECK out of the box.
┌─────────────────────────────────────────────────────────────┐
│ Option A: Plain Docker │
│ ───────────────────── │
│ One container. memory mode. Zero extra infra. │
│ The simplest supported path. │
│ │
│ Option B: OCI Always Free │
│ ───────────────────── │
│ One Always Free VM + Docker + Caddy in us-ashburn-1. │
│ GitHub-driven production deploys target this path only. │
│ See docs/oci-always-free.md for the CLI runbook. │
│ │
│ Option C: AWS / CDK (manual) │
│ ─────────────────────── │
│ CloudFront + WAF + Basic Auth + EC2 + nginx + Docker. │
│ See cdk/README.md if intentionally bringing up AWS. │
│ No automatic GitHub deployment workflow is enabled. │
└─────────────────────────────────────────────────────────────┘
- Operational runbook → docs/operations-runbook.md
- OCI Always Free runbook → docs/oci-always-free.md
- Branch protection + CI gates → docs/branch-protection.md
OCI Ampere A1 is Arm-based. Build a linux/arm64 image on the VM or publish a
multi-arch image before using that path; if A1 capacity is unavailable, the
Always Free VM.Standard.E2.1.Micro fallback can run the current linux/amd64
Docker Hub image.
YASP is intentionally no-auth:
- Room URLs are bearer-style meeting links
sessionIdis continuity, not identity proof- Moderators are a room-level role, not an authenticated account
Within that boundary, hardening includes:
CSP + browser security headers Input validation + abuse shaping
Non-root container image Hardened runtime flags (--cap-drop ALL)
Healthcheck-based deploy rollback Layered CI security scanning
What YASP does not claim:
- Strong user authentication
- Durable privacy beyond bearer-link secrecy
- History, audit trails, or persistence
- True multi-instance readiness
Security docs → SECURITY_THREAT_MODEL.md · SECURITY_AUDIT_REPORT.md · docs/security-scanning.md
Blocking checks — these must pass before any merge:
| Check | What it covers |
|---|---|
validate |
Translations · lint · build · tests · format |
a11y-smoke |
Playwright accessibility smoke |
docker-validation |
Production image build + healthcheck |
cdk-synth |
CDK stack synthesis (on cdk/ changes) |
CodeQL |
Security query pack (JS/TS) |
Advisory lanes (visible, not yet blocking): dependency review · Trivy scans · npm audit · strict lint · Knip · OSSF Scorecard.
Every PR gets two advisory signals: client bundle size report and a 7-day preview artifact of client/dist/.
Full details → docs/security-scanning.md
✓ Keyboard-operable core flows
✓ Semantic landmarks + route-aware document titles
✓ Live-region announcements for room state changes
✓ Reduced-motion handling
✓ Forced-colors fallbacks
✓ Automated smoke coverage via npm run test:a11y
YASP should not be described as WCAG-conformant yet. Automated and browser/manual QA is complete for core flows; real assistive-technology validation is still outstanding in some areas.
Audit docs → ACCESSIBILITY_WCAG_2_2_AAA_AUDIT.md · ACCESSIBILITY_MANUAL_QA_CHECKLIST.md
Powered by i18next + react-i18next. English is the source and fallback locale. npm run i18n:check enforces key parity in CI.
| Locale | Locale | ||
|---|---|---|---|
| 🇺🇸 | en — English |
🇯🇵 | ja — Japanese |
| 🇪🇸 | es — Spanish |
🇰🇷 | ko — Korean |
| 🇫🇷 | fr — French |
🇨🇳 | zh-Hans — Simplified Chinese |
| 🇩🇪 | de — German |
🇹🇼 | zh-Hant — Traditional Chinese |
| 🇧🇷 | pt — Portuguese |
Translator terminology guide → docs/i18n-glossary.md
Recovery UI only appears when the live room connection is unhealthy — the happy path stays completely silent.
Disconnected? → Retry (standard reconnect attempt)
→ Compatibility (polling transport fallback for this tab)
→ Details (non-sensitive diagnostics for support)
Common causes: browser extensions, VPNs, proxies, or network policies interfering with WebSocket upgrades.
- Moderators get
View round reportafter reveal — CSV / JSON / Print export available - Participants get
View round summary— view-only, no export - Resetting or advancing the round removes the current report entry point
Export before reset/next round if you need to keep the data.
Want to contribute? See CONTRIBUTING.md for the full guide.
Quick checklist before submitting a PR:
- Read plans/next-up.md and plans/open-questions.md
- Check accepted ADRs in plans/decisions/
- Run
npm test && npm run lint && npm run build - Update docs/plans if product or operational behavior changed
AI-agent repo rules → AGENTS.md
MIT — see LICENSE. Copyright 2026 William Leonhardt.
Pull it. Run it. Estimate. Shut it down. Done.