βββ βββ ββββββ βββββββββββββββ ββββ ββββββββββββββββββββββββββββ βββββββ ββββββββββββββββββββββββ βββββ βββββββββββββββββββββββ βββ βββ ββββββββββββββ βββ βββ ββββββββββββββ
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.