Skip to content

wleonhardt/YASP

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

205 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

β–ˆβ–ˆβ•—   β–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— 
β•šβ–ˆβ–ˆβ•— β–ˆβ–ˆβ•”β•β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ•”β•β•β•β•β•β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—
 β•šβ–ˆβ–ˆβ–ˆβ–ˆβ•”β• β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β•
  β•šβ–ˆβ–ˆβ•”β•  β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•‘β•šβ•β•β•β•β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•”β•β•β•β• 
   β–ˆβ–ˆβ•‘   β–ˆβ–ˆβ•‘  β–ˆβ–ˆβ•‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘     
   β•šβ•β•   β•šβ•β•  β•šβ•β•β•šβ•β•β•β•β•β•β•β•šβ•β•     

πŸƒ Yet Another Scrum Poker

CI Docker Pulls Image Size Node 20 License MIT OpenSSF Best Practices

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.


Who Is This For?

  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  β”‚                                                             β”‚
  β”‚   πŸ§‘β€πŸ’»  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       β”‚
  β”‚                                                             β”‚
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

How a Room Works

  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 Highlights

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

πŸš€ Quick Start

docker run --rm -p 3001:3001 wleonhardt/yasp:main

Open β†’ 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

☁️ Ephemeral by Design

  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  β”‚   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.


🧰 Run It Your Way

One-off session β€” gone on Ctrl-C:

docker run --rm -p 3001:3001 wleonhardt/yasp:main

Persistent background service β€” survives reboots:

docker run -d --restart unless-stopped --name yasp -p 3001:3001 wleonhardt/yasp:main

Build locally:

docker build -t yasp:local .
docker run --rm -p 3001:3001 yasp:local

Apple Silicon: add --platform linux/amd64 if you need the x86_64 image target.


πŸ—οΈ Architecture

  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  β”‚                        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.


πŸ“ Repository Layout

  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

πŸ”§ Local Development

Prerequisites: Node.js 20+, npm 9+

git clone https://github.com/wleonhardt/YASP.git yasp
cd yasp
npm install
npm run dev

Starts two processes:

  http://localhost:3001  ←  Fastify + Socket.IO server
  http://localhost:5173  ←  Vite dev client (hot reload)

Commands

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.


βš™οΈ Configuration

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

πŸ“‘ Runtime Profiles

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.


🐳 Docker Image

  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:main

❀️ Health Endpoint

GET /api/health  β†’  { "ok": true }
# Docker Compose healthcheck
healthcheck:
  test: ["CMD", "curl", "-sf", "http://localhost:3001/api/health"]
  interval: 30s
  timeout: 5s
  retries: 3

The image ships a HEALTHCHECK out of the box.


☁️ Deployment

  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  β”‚  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.        β”‚
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

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.


πŸ”’ Security Posture

YASP is intentionally no-auth:

  • Room URLs are bearer-style meeting links
  • sessionId is 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


βœ… CI & Quality Gates

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


🦾 Accessibility

  βœ“ 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


🌍 Localization

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


πŸ” Realtime Recovery

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.


πŸ“‹ Round Reports

  • Moderators get View round report after 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.


🀝 Contributing

Want to contribute? See CONTRIBUTING.md for the full guide.

Quick checklist before submitting a PR:

  1. Read plans/next-up.md and plans/open-questions.md
  2. Check accepted ADRs in plans/decisions/
  3. Run npm test && npm run lint && npm run build
  4. Update docs/plans if product or operational behavior changed

AI-agent repo rules β†’ AGENTS.md


πŸ“„ License

MIT β€” see LICENSE. Copyright 2026 William Leonhardt.


  Pull it.  Run it.  Estimate.  Shut it down.  Done.

About

Yet Another Scrum Poker

Resources

License

Contributing

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors