Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

19 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

πŸ‡¬πŸ‡§ English Β· πŸ‡©πŸ‡ͺ Deutsch

🍺 BeerPong 3000

CI License: MIT Live demo React TypeScript Node.js Socket.IO

Run a full beer pong tournament for up to 60 teams straight from your phone: automatic scheduling across as many tables as you have, a live dashboard your friends can follow on a projector, QR-code sign-up for players (no app, no account, no signup friction), and push notifications when it's your turn to play. Free, open source, and self-hostable.

🌐 Try it live: beerpong.klaiber.dev - no installation needed, just scan a code and play. Want to run your own instance? See Docker below.

Table of Contents

Features

  • Organizer accounts: organizers register with email + password and log in to create and manage tournaments; a session cookie keeps them signed in across visits. Players never need an account - they stay fully anonymous, identified only by a per-device token.
  • Organizer flow: once logged in, create a tournament (name, location, info text, team size, max. teams, number of tables, format, group size, qualifiers/group, standard round time limit) - it immediately shows up in "My tournaments" on the home page.
  • Access only via QR code or 8-character code (unambiguous characters, no 0/O/1/I). Participants scan the QR code, enter their name, and create a team or join an open one. Once a team reaches its configured size, it's "full" and disappears from the join list. With a team size of 1, teams are automatically called "Participants" instead of "Team".
  • Two tournament formats:
    • Group stage + knockout: round-robin group stage, then a knockout phase with the best teams (snake seeding - group winners face runners-up from other groups), including byes and an optional 3rd place match.
    • Free bracket: no group stage - once registration closes, all teams are randomly seeded directly into the knockout bracket. Good for smaller or spontaneous tournaments.
  • Standard round time limit: the organizer can set a time limit (in minutes) for group stage matches. Once a table runner taps "Start match", a countdown runs there; when it expires, a push notification and a red banner appear on the table view.
  • Live table dispatcher: as soon as a table frees up, the next match that's ready to play is assigned to it automatically - no rigid printed schedule needed, works with any number of tables in parallel.
  • Push notifications (installable PWA, works on Android and iOS): teams are automatically notified when they're called to a table or their round time runs out - even while the app is in the background.
  • Multilingual (English/German): the interface automatically detects the browser/system language (default: English) and can be switched manually at any time via a toggle in the top right corner.
  • Live dashboard (projector/TV friendly): current table occupancy, overall and group standings, knockout bracket - all live via WebSocket.
  • Table view: a large, dedicated touch interface per table (with its own QR code to print and stick on) to start a match and enter the result.
  • Admin dashboard: tournament control (open/close registration, start group/knockout stage, finish tournament), table management (add/disable/delete), team management (rename, remove players, delete team), quick result entry for all live matches, CSV export of the final standings.
  • Multiple tournaments: any number of independent tournaments per organizer account, each with its own access code.

Organizer Guide

("Organizer"/"Spielleiter" - the person running the tournament.)

  1. Create an account. Open the app, click Register and sign up with an email and password. You're logged in immediately - no email confirmation step.
  2. Create a tournament. From the home page, click + Create new tournament. Fill in a name and, optionally, a location and info text (rules, prizes, kickoff time - shown to participants). Choose:
    • Team size (1 = solo/individual mode - participants sign up alone instead of forming teams).
    • Max. teams and number of tables.
    • Format: Group stage + knockout (round-robin groups, then a bracket with the best teams - recommended from ~8 teams) or Free bracket (no groups, everyone goes straight into a randomly-seeded knockout bracket - good for smaller or spontaneous tournaments).
    • For the group-stage format: teams per group, qualifiers per group, and an optional standard round time limit (minutes) for group matches.
  3. Share access. The admin dashboard shows a QR code and an 8-character access code. Share either however you like - print the QR code, message the code, or display it on a projector. Participants never need an account to join.
  4. Open registration. Click Open registration once you're ready for signups. Teams appear live in the admin dashboard as people join; you can remove a player or delete a team at any point if needed.
  5. Start the tournament. Once you have at least 2 teams, click Close registration & start group stage (or & draw knockout bracket for the free-bracket format). From here on, the live table dispatcher takes over: as soon as a table is free, the next ready match is assigned to it automatically.
  6. Let matches happen. Whoever's at a table taps Start match there to begin the timer (if you set one), then enters the final score. You can also enter results yourself for any match currently at a table, directly from Live matches in the admin dashboard - no need to walk around with a phone at every table.
  7. Advance to the knockout stage (group-stage format only). Once every group match is done, click Start knockout stage - the bracket is drawn automatically (snake-seeded by group standings, byes handled for you, plus an optional 3rd-place match).
  8. Finish up. After the final is played, click Finish tournament to crown the champion (confetti included πŸŽ‰), then Export CSV to download the final standings.
  9. Manage tables anytime from the admin dashboard: add, disable, or delete tables, and print each table's own QR code so players can jump straight to that table's screen.

Participant Guide

("Participant"/"Teilnehmer" - anyone signing up to play. No account needed, ever.)

  1. Join. Scan the tournament's QR code, or open the app and enter its 8-character access code on the home page.
  2. Sign up. Create a new team (try the 🎲 button next to the team-name field for a random name suggestion) or join one that's still open. In solo-mode tournaments you just enter your own name - no team name needed. Once a team reaches its configured size, it's full and disappears from the join list for everyone else.
  3. Turn on notifications. Right after signing up, tap πŸ”” Push notifications on your team page so you don't miss your call to a table. This works directly on Android; on iPhone, Safari only allows it after adding the site to your Home Screen (Share β†’ Add to Home Screen) - the app detects this and shows you the exact steps instead of a button that wouldn't do anything.
  4. Wait for your match. Your team page always shows your next match. The moment a table opens up for you, you'll get a push notification - even if the app is in the background.
  5. Play. At the table, tap Start match to begin (and start the round timer, if the organizer set one), then enter the final score once you're done - a tie isn't possible in beer pong, so there must be a winner.
  6. Follow along live. Open the Live Board at any time (no login needed) to check the current bracket, standings, and which tables are occupied.
  7. Install the app (optional). A banner will offer to add BeerPong 3000 to your home screen for quicker access and reliable notifications - handy if you're playing multiple rounds.

Tech Stack

  • Server: Node.js, Express, Socket.IO, Prisma + SQLite, TypeScript, web-push (VAPID)
  • Client: React, Vite, TypeScript, Tailwind CSS, React Router, socket.io-client, i18next/react-i18next, qrcode.react, a hand-rolled Service Worker (no framework)

Project Structure

beerpong-tournament/
β”œβ”€β”€ server/               Express + Socket.IO API, Prisma/SQLite
β”‚   β”œβ”€β”€ prisma/           Schema + migrations
β”‚   └── src/
β”‚       β”œβ”€β”€ routes/       REST endpoints (tournaments, teams, matches, push)
β”‚       β”œβ”€β”€ services/     Scheduler (schedule/bracket), standings, push, codes
β”‚       β”œβ”€β”€ middleware/   Admin auth
β”‚       └── sockets.ts    WebSocket broadcast
β”œβ”€β”€ client/               React SPA + PWA
β”‚   β”œβ”€β”€ public/           manifest.webmanifest, sw.js (service worker), icons
β”‚   └── src/
β”‚       β”œβ”€β”€ i18n/         Translation resources (en.json, de.json) + language setup
β”‚       β”œβ”€β”€ pages/        Home, Create, Join, ParticipantHub, AdminDashboard,
β”‚       β”‚                 TableView, LiveDashboard
β”‚       β”œβ”€β”€ components/   UI building blocks (cards, buttons, MatchCard, timer, ...)
β”‚       └── lib/          API client, socket hook, push helper, storage
└── .github/workflows/    CI/CD (Docker build + deploy)

Local Development

Requirement: Node.js β‰₯ 20.

npm install
npm run prisma:migrate   # creates server/dev.db and applies all migrations
npm run dev               # starts server (port 4000) and client (port 5173) in parallel

Then open http://localhost:5173 (the Vite dev server automatically proxies /api and /socket.io to the server on port 4000).

Testing

An end-to-end Playwright suite in e2e/ drives the real app in a browser against an isolated SQLite database (server/prisma/test.db, never touching your dev data) - full tournament lifecycles (both formats, byes, the live table dispatcher, standings tiebreaks, the visual bracket), i18n/locale detection, the PWA install prompt, and admin/real-time sync.

npx playwright install --with-deps chromium   # once
npm run test:e2e                              # headless
npm run test:e2e:headed                       # watch it run

Environment Variables

All variables live in server/.env (see server/.env.example):

Variable Required Description
DATABASE_URL yes SQLite path, e.g. file:./dev.db
PORT no Server port, defaults to 4000
VAPID_PUBLIC_KEY no* Public key for Web Push
VAPID_PRIVATE_KEY no* Private key for Web Push (keep secret!)
VAPID_SUBJECT no mailto: address identifying the push sender
CORS_ALLOWED_ORIGINS no Comma-separated extra origins. Empty (default) = same-origin only
RATE_LIMIT_API_MAX no API requests per minute per IP, defaults to 1200
RATE_LIMIT_LOGIN_MAX no Failed logins per account per 15 min, defaults to 10
RATE_LIMIT_LOGIN_IP_MAX no Failed logins per IP per 15 min, defaults to 100
RATE_LIMIT_REGISTER_MAX no New accounts per IP per hour, defaults to 20

* Without VAPID keys the app still runs normally, only push notifications are disabled (the server logs a warning). Generate your own keys:

npx web-push generate-vapid-keys

Internationalization

The interface is fully available in English and German (client/src/i18n/en.json and de.json). On first load the app automatically detects the browser/system language; if it isn't German, English is used as the default. Anyone can switch the language manually at any time via the toggle in the top-right corner of the header (the choice is remembered locally).

Server-generated content (tournament/team/player names, table names, group and round labels like "Semifinal") is deliberately not translated - just like any other user-generated content. API error messages are transmitted as stable error codes and displayed in the client in the currently selected language.

To add another language: create a new resource file following the pattern of en.json in client/src/i18n/, register it under resources in client/src/i18n/index.ts, and add it to supportedLngs.

Push Notifications (PWA)

The app is a fully-fledged Progressive Web App with its own service worker (client/public/sw.js) and web app manifest (client/public/manifest.webmanifest):

  • Android (Chrome/Edge): push works directly in the browser tab; installing to the home screen is optional (but recommended for the full app look and feel).
  • iOS (Safari): Apple only allows web push for pages that have been added to the home screen (Share β†’ "Add to Home Screen"). The app detects this automatically and shows iPhone users the right instructions instead of a button that wouldn't do anything.

Notifications sent:

  1. "You're up!" - as soon as the live table dispatcher assigns a team to a free table.
  2. "Time's up!" - as soon as the organizer's standard round time limit is reached for a running group stage match (checked server-side every 15 seconds).

Participants enable push via the "πŸ”” Push notifications" card on their personal team page, right after signing up.

Production Build

npm run build
node server/dist/index.js   # serves the built React app and the API together

Docker

docker build -t beerpong-3000 .
docker run -p 8080:80 \
  -e VAPID_PUBLIC_KEY=... -e VAPID_PRIVATE_KEY=... -e VAPID_SUBJECT=mailto:you@example.com \
  -v beerpong-data:/data \
  beerpong-3000

The image is a multi-stage build: the server and client are built, and the final image only runs the Node server (port 80), which also serves the static React app. The SQLite database lives at /data/dev.db (mount it as a volume, otherwise data is lost when the container restarts).

Data Model

Prisma schema (server/prisma/schema.prisma):

  • User / Session - organizer accounts (email + bcrypt password hash) and their login sessions (DB-backed, so logging out actually invalidates the session)
  • Tournament - tournament metadata, format, time limit, access code, owning organizer (organizerId)
  • Table - tables (free/in use/disabled) plus a secret token that is embedded in the table's QR code and authorises entering results at that table
  • Team / Player - teams and their members (a player token identifies the device)
  • Group - group stage groups (only for the "group stage" format)
  • Match - match pairings including knockout bracket chaining (nextMatchId, loserNextMatchId for the 3rd place match)
  • PushSubscription - web push subscriptions per player/device

Tournament Flow in Detail

  1. The organizer logs in, creates the tournament (status DRAFT), and opens registration (REGISTRATION).
  2. Participants join via QR code/access code, create or complete teams.
  3. The organizer closes registration:
    • Group stage: teams are randomly distributed into groups, a round-robin schedule is generated, status becomes GROUP_STAGE.
    • Free bracket: teams are randomly seeded directly into a knockout bracket, status jumps straight to KNOCKOUT.
  4. The live dispatcher continuously assigns free tables to the next matches ready to play; teams are notified via push.
  5. Once the group stage is complete, the organizer generates the knockout stage (snake seeding by group standings, including byes and a 3rd place match).
  6. After the final, the organizer finishes the tournament (FINISHED); results can be exported as CSV.

License

MIT - do whatever you want with it, attribution appreciated but not required.

About

Run beer pong tournaments with live scheduling, QR-code sign-up, and push notifications. Free, open-source, self-hostable PWA (React, Node, Socket.IO).

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages