I Built a Snooker Scoring PWA Because the Game Room Scoreboard Only Had 2 Slots
We were three friends. One scoreboard. Zero solutions.
Every time my friends and I hit the snooker table, we'd run into the same problem — the scoreboard on the wall was built for two players only. One of us always had to manually keep score in their head, or on a piece of paper, or just... trust the other two. Not ideal when points are on the line.
So I did what any developer would do. I built an app.
That app is SnookerBee — a mobile-first Progressive Web App for tracking snooker scores, built with React 19, TypeScript, Vite, Supabase, and Google OAuth. It's live at snookerbee.vercel.app and the full source is on GitHub.
Here's how I built it, what I learned, and why a PWA was the right choice.
The Problem
Snooker is typically played 1v1. Most scoreboards, apps, and trackers assume this. But snooker rooms often have 3-4 people sharing a table in a free-for-all — rotating turns, individual scores, tracking who won which frame.
I couldn't find an app that:
Supported more than 2 players properly
Worked offline (game rooms often have spotty Wi-Fi)
Felt designed for snooker specifically — not a generic "sports tracker"
Was installable on mobile without going through an App Store
So I decided to build exactly that.
Tech Stack
LayerTechnologyFrontendReact 19 + TypeScriptBuild ToolViteRoutingReact Router v7StylingVanilla CSS (custom tokens, glassmorphism)PWAvite-plugin-pwa + WorkboxAudioWeb Audio APIBackendSupabase (PostgreSQL)AuthGoogle OAuth via Supabase AuthHostingVercel
Features
🎮 Three Game Modes
1v1 — Standard head-to-head
Teams — 2v2 or 3v3 alternating configurations
Free-for-All — 2 to 8 players, round-robin turns
This was the core reason I built it — Free-for-All mode handles exactly the situation my friends and I were in.
⚖️ Strict Rules Engine
The scoring logic is built as a pure state-machine reducer. It handles:
Official red-color sequence enforcement
Free ball nomination
Re-spotted black on tied scores
Turn rotation management across all game modes
10-step deep-cloned undo stack
Getting this right was honestly the hardest part of the project. Snooker rules have a lot of edge cases — especially around fouls, free balls, and end-game scenarios.
🔊 Sound Effects — Zero Dependencies
Instead of importing a sound library, I used the Web Audio API directly — oscillators, gains, and envelopes — to synthesize:
Pot confirmation chimes
Foul warning buzzes
Break milestone sounds (25, 50, 100 points)
Victory fanfare
This keeps the bundle lean and works completely offline.
📶 Offline-First PWA
Using vite-plugin-pwa with Workbox caching strategies, the app:
Installs directly to your phone homescreen
Works without internet after first load
Has a custom app icon
This was critical for the game room use case — you don't want your scoring app to fail mid-match because of Wi-Fi.
☁️ Cloud Sync with Supabase
When you're signed in with Google, SnookerBee syncs:
Full match history
Player statistics (highest break, frames won, fouls)
Frame-level action logs
The database uses Row Level Security (RLS) so each user can only access their own data.
Database Schema
Three tables power the cloud sync:
sql-- Matches table
create table matches (
id uuid primary key default gen_random_uuid(),
user_id uuid references auth.users(id) on delete cascade,
mode text not null,
reds_count int not null,
best_of int not null,
created_at timestamptz default now(),
duration_ms bigint not null,
winner_name text not null
);
-- Players per match
create table match_players (
id uuid primary key default gen_random_uuid(),
match_id uuid references matches(id) on delete cascade,
player_name text not null,
team_name text,
total_score int default 0,
highest_break int default 0,
frames_won int default 0,
fouls_committed int default 0,
time_spent_ms bigint default 0
);
-- Frame-level logs
create table match_frames (
id uuid primary key default gen_random_uuid(),
match_id uuid references matches(id) on delete cascade,
frame_number int not null,
duration_ms bigint,
action_log jsonb
);
All three tables have RLS enabled with user-scoped policies.
Lessons Learned
PWA is underrated for sports apps
The offline capability + homescreen install covers 90% of what a native app gives you, without the App Store overhead. For a game room tool, this is ideal.Web Audio API is powerful and underused
I expected to need a library for sounds. Turns out the native API handles everything I needed with less than 100 lines of code, and zero extra bundle weight.State machines for game logic are worth the upfront cost
I almost built the scoring logic with scattered useState calls. Switching to a reducer-based state machine early saved me from dozens of bugs later — especially with undo functionality.Supabase RLS is your friend
Row Level Security meant I never had to write server-side authorization code. The database enforces it at the query level.
What's Next
Android app via TWA (Trusted Web Activity) / Bubblewrap — Play Store submission
iOS app via Capacitor.js wrapper
Tournament mode — bracket-style multi-match tournaments
Bluetooth scoreboard integration (experimenting)
Try It
🌐 Live App: snookerbee.vercel.app
💻 GitHub: github.com/arshad0126/snookerbee
If you play snooker with more than one friend — give it a try. And if you find a bug or want to contribute, PRs are welcome.
Built with React 19, TypeScript, Vite, Supabase, and a lot of missed pots.
Tags: #webdev #react #pwa #supabase #typescript #buildinpublic #javascript #opensource
Top comments (0)