Skip to content

joehunt/lean-coffee

Repository files navigation

☕ Lean Coffee

Run democratic, timeboxed Lean Coffee sessions — in-person and remote — from a single command.

Participants scan a QR code, submit topics, and upvote from their phones. The app picks the top-voted topic, runs a server-authoritative timer, and polls the room to keep going or move on — while a built-in voice moderator reads each topic aloud and calls time.

CI License: MIT Node TypeScript PRs Welcome


Table of contents


What is Lean Coffee?

Lean Coffee is a structured-but-agenda-less meeting format. People propose topics, vote on what to discuss, and work through them one at a time in short timeboxes. When a timer ends the group does a quick thumbs vote: keep going or move on. It keeps conversations focused and democratic — everyone's topic gets a fair shot at the table.

This app runs that ritual for a hybrid room: a facilitator drives a Host Console (great on a projector / shared screen), and everyone else joins from a phone or laptop with no login.

Features

  • 🗳️ Submit & upvote topics in real time — one vote per topic, unlimited topics, live re-sorting
  • ⏱️ Server-authoritative timers — every device counts down in perfect sync; survives refreshes & reconnects
  • 🙋 Keep-going / move-on polls — opens automatically at time's-up, with a live tally
  • 📱 Frictionless join — scan a QR or open a link; anonymous, optional display name
  • 🌍 Remote-ready by default — one command opens a public Cloudflare tunnel so remote folks join while you screen-share (no account, no config)
  • 🗣️ Voice moderator (host only) — reads each topic aloud and announces "time's up" using the browser's speech synthesizer
  • 🧲 Auto-scroll — the Host Console keeps the active topic + timer in view as the discussion advances
  • ♻️ Concurrent by design — add and upvote topics any time, even mid-discussion
  • 🎨 Clean, modern UI — light/dark, mobile-first participant view, projector-friendly host view
  • 💾 Zero-setup persistence — embedded SQLite; a laptop sleep or restart recovers the session

Quick start

Prerequisites: Node.js ≥ 20. For remote attendees, cloudflared (e.g. brew install cloudflared) — optional; the app falls back to your LAN address without it.

git clone https://github.com/joehunt/lean-coffee.git
cd lean-coffee
npm install
npm start

Open the printed Host Console URL (http://localhost:3000/host/<id>?t=<token>), put it on the shared screen, and have everyone scan the QR. That's it.

  • npm start — builds the web app, boots the server on :3000, and opens a public tunnel so remote attendees can join.
  • npm run start:local — same, but LAN-only (no tunnel).

How it works

  Create a session  ──▶  Share the QR / link  ──▶  Collect & upvote topics
        │                                                     │
        ▼                                                     ▼
  Start discussion  ◀──  Pick the top-voted topic  ◀──  (anyone, any time)
        │
        ▼
  Run the timebox (default 5 min)  ──▶  Time's up  ──▶  "Keep going?" poll
        │                                                     │
        └──────────  Extend (+2 min)  ◀── facilitator ──▶  Next topic

As the facilitator you: create the session, open submissions, start the discussion, and use the stage controls (pause, ±1 min, extend, next, start poll). The app auto-selects the top-voted topic and runs everything else.

As a participant you: scan the QR, optionally type a name, add topics, upvote the ones you care about, and tap Keep going / Move on when a poll is live — all from your phone.

Running for remote attendees

npm start opens a Cloudflare Quick Tunnel automatically — no account, no signup, no config:

  1. The server spawns cloudflared and parses the public https://<random>.trycloudflare.com URL.
  2. The Host Console's QR and join link switch to that public URL (the card shows "Public tunnel live").
  3. Anyone you share your screen with on Zoom / Webex / Meet can scan the same QR and join over the internet.

If cloudflared isn't installed or can't connect, the app gracefully falls back to your LAN address (the card shows "Local network") — perfect for same-room/same-Wi-Fi sessions. Use npm run start:local to skip the tunnel entirely.

Note: Quick Tunnel URLs are ephemeral (a new one each run) — ideal for ad-hoc sessions. For a stable recurring URL, a named Cloudflare tunnel (free account) is a natural opt-in.

Configuration

Environment variables:

Variable Default Description
PORT 3000 HTTP/WebSocket port
DB_PATH data/lean-coffee.db SQLite database file
TUNNEL 1 (on) Set TUNNEL=0 to disable the Cloudflare tunnel

Per-session settings (chosen on the create-session screen, or via the API):

Setting Default Description
defaultTimerSec 300 (5 m) Length of each discussion timebox
extensionSec 120 (2 m) Length added by Extend
maxVotesPerSession null Optional cap on distinct topics one person can upvote (unlimited)

Architecture

The whole app is one Node process: Fastify serves the built React SPA and the REST endpoints, Socket.IO handles realtime, and SQLite stores state — no external services required.

Architecture diagram

The server is the single source of truth. Clients are thin renderers: they send intents (topic:add, host:startDiscussion, …) and receive authoritative broadcasts (session:snapshot, round:updated, …). Timers live on the server as absolute endsAt timestamps; clients compute the remaining time from endsAt plus a clock-offset handshake, so every screen stays in lockstep and reconnects heal automatically.

Layer Technology
Server Fastify + Socket.IO
Persistence SQLite via better-sqlite3 (WAL)
Frontend React + Vite + Tailwind CSS + Zustand
Realtime WebSocket (polling-first, upgrades to WS) over Socket.IO rooms
Tunnel Cloudflare Quick Tunnel (cloudflared)
Language TypeScript (strict), ESM

See docs/ARCHITECTURE.md for the data model, the event protocol, and the discussion state machine — and docs/SPEC.md for the full design spec.

Project structure

lean-coffee/
├── shared/protocol.ts     # Single source of truth: event names, payloads, domain types
├── server/
│   ├── index.ts           # Fastify + Socket.IO bootstrap, LAN/tunnel wiring
│   ├── engine.ts          # Pure, testable state machine + timer math
│   ├── state.ts           # SessionManager: rooms, snapshots, timer scheduling
│   ├── repo.ts            # SQLite repository (WAL)
│   ├── handlers.ts        # Socket intent handlers + host-token authorization
│   ├── http.ts            # REST routes (create session, healthz, QR)
│   └── tunnel.ts          # Cloudflare quick-tunnel spawner
├── web/src/
│   ├── views/             # Landing, HostConsole, Participant, JoinGate
│   ├── components/        # CircularTimer, JoinCard, TallyBars, VoiceToggle, …
│   ├── lib/               # socket client, speech, time, storage
│   └── store.ts           # Zustand store fed by Socket.IO events
└── tests/                 # Vitest unit (engine) + integration (live server) suites

Development

npm run dev         # Vite dev server + tsx watch (HMR), proxying API/WS to the server
npm run typecheck   # tsc --noEmit (strict)
npm test            # vitest run
npm run build       # production build to dist/

When iterating with npm start, remember it builds the SPA and serves it from dist/; after editing client code, rebuild (or use npm run dev for HMR).

Testing

npm test runs 37 tests across two suites:

  • Unit (tests/engine.test.ts) — the pure engine: voting rules, top-topic selection & tie-breaks, timer math (pause/resume, ±time, extension), and the discussion state machine.
  • Integration (tests/integration.test.ts) — boots a real Fastify + Socket.IO server on an ephemeral port and drives the full flow with Socket.IO clients: join → snapshot, topic add/vote broadcasts, one-vote-per-topic enforcement, host-only authorization, start → top-topic selection, continue poll & tally, extend/next, and timer-expiry auto-transition.

Roadmap

  • Per-session vote budget UI (server enforcement already exists)
  • Drag-reorder queue, park topics, "discuss now" override polish
  • Session summary export (topics, votes, time spent) as Markdown
  • Auto-advance suggestion when "move on" passes a threshold
  • Emoji reactions and per-topic notes / action items
  • Optional named Cloudflare tunnel for a stable recurring URL

Contributing

Contributions are welcome! Please read CONTRIBUTING.md and our Code of Conduct. Good first steps: pick a Roadmap item, open an issue to discuss, and send a focused PR with npm run typecheck && npm test passing.

Security

Found a vulnerability? Please follow the disclosure process in SECURITY.md rather than opening a public issue.

License

MIT © Joe Hunt


Built as a demonstration of agentic software development — designed, implemented, tested, and verified end-to-end. See the design spec for the full story.

About

Run democratic, timeboxed Lean Coffee sessions — in-person and remote — from one command. Real-time topics & voting, server-authoritative timers, QR join, public Cloudflare tunnel, and a voice moderator.

Topics

Resources

License

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages