How I built a live ball-by-ball cricket scorer and SVG telemetry dashboard using Vanilla JS, Firebase Realtime Database, and GitHub Actions secret injection.
Local cricket tournaments have a universal problem: one person is keeping score in a paper book (or shouting it across the ground), while players in the dugout and friends watching from home have no idea what the required run rate is or who is bowling.
Existing scoring apps either force every spectator to install a 100MB mobile app and hand over their phone number, or lock basic scorecards behind a subscription.
I built Scoreboard to solve this with zero friction: a single-file, zero-build web application that turns one phone into an umpire's scoring console and every other phone into a live broadcast scoreboard.
- Live App: https://ajith-poojari.github.io/Cric-scorer/
- GitHub Repository: https://github.com/ajith-poojari/Cric-scorer
Here is a breakdown of the architecture and the most interesting technical hurdles I solved along the way.
1. Why Single-File Vanilla JS?
It's tempting to reach for Next.js or Vite + React for every project. Instead, I constrained the entire app to a single index.html file with zero npm dependencies.
Why? Forkability and zero-maintenance longevity.
If a college sports club wants to host their own private scoreboard 3 years from now, they shouldn't have to fight broken node_modules or deprecated build plugins. They can fork the repository, add their Firebase secrets, and deploy directly to GitHub Pages.
2. Injecting Firebase Credentials Safely via GitHub Actions
While Firebase client configuration is public in the browser bundle, hardcoding keys in a public GitHub repository triggers security alerts and makes it annoying for others to fork.
To keep the repository clean, index.html ships with placeholder tokens:
javascript const firebaseConfig = { apiKey: "__FIREBASE_API_KEY__", authDomain: "__FIREBASE_AUTH_DOMAIN__", databaseURL: "__FIREBASE_DATABASE_URL__", projectId: "__FIREBASE_PROJECT_ID__", // ... }; During deployment, .github/workflows/deploy.yml injects the repository secrets right before packaging the GitHub Pages artifact: - name: Inject Firebase config from secrets env: FIREBASE_API_KEY: ${{ secrets.FIREBASE_API_KEY }} FIREBASE_DATABASE_URL: ${{ secrets.FIREBASE_DATABASE_URL }} run: | sed -i "s|__FIREBASE_API_KEY__|${FIREBASE_API_KEY}|g" index.html sed -i "s|__FIREBASE_DATABASE_URL__|${FIREBASE_DATABASE_URL}|g" index.html And if someone opens index.html on localhost before those placeholders are replaced, the app detects firebaseConfig.databaseURL.startsWith('__FIREBASE_') and falls back to localStorage + cross-tab storage events so local development works out of the box. 3. Modeling Cricket's State Machine Cricket has some of the most interconnected state transitions of any sport: Strike Rotation: Batters swap ends on odd runs (1, 3) and at the end of every 6 legal balls. If a batter runs 1 on the 6th legal ball of an over, they swap twice—staying on strike for the next over. Illegal Deliveries with Runs: A Wide (Wd) or No Ball (Nb) adds 1 penalty run and does not increment the legal ball count. However, if batters run extra byes on a Wide, or hit a boundary off a No Ball, those runs must be credited properly (to extras for Wides, to the batter for No Balls) and odd extra runs must still rotate the strike. Free Hit Protection: Logging a No Ball sets state.freeHit = true for the next delivery. If a wicket is logged on a Free Hit, standard bowler dismissals (Bowled, Caught, LBW, Stumped, Hit Wicket) are blocked, while Run Out remains valid. Bowler Quotas: In T20, no bowler can exceed 4 overs (2 in T10, 10 in ODI), and no bowler can bowl two consecutive overs. At the end of every over, the bowler selector automatically filters out previousBowlerId and any bowler who has reached their format quota. To make mistakes painless, every delivery pushes a JSON snapshot of the match state onto a 60-deep history stack before mutating state—allowing single-tap Undo even across over boundaries or innings breaks. 4. The Firebase Empty-Array Gotcha One subtle bug with Firebase Realtime Database is that it does not store empty arrays ([]). If you write { deliveries: [], fow: [] } when an innings starts at 0.0 overs, Firebase strips those keys. When a viewer connects at 0.0 overs, inn.deliveries comes back undefined. To prevent viewer crashes, every remote snapshot passes through a normalizer before merging into state: function normalizeMatchData(data if (!data) return data; const copy = Object.assign({}, data); copy.innings = (copy.innings || []).map(inn => Object.assign({ deliveries: [], fow: [], dismissedOrder: [] }, inn, { deliveries: inn.deliveries || [], fow: inn.fow || [], dismissedOrder: inn.dismissedOrder || [] })); return copy; } 5. Zero-Dependency SVG Telemetry (Worm, Manhattan & Run Rate Graphs) Rather than bundling Chart.js or D3, the scoreboard builds responsive <svg viewBox="0 0 680 270"> visualizations directly from the delivery log: Worm Graph: Plots cumulative runs ball-by-ball for both innings with linear gradient area fills, a dashed target line, and crimson W nodes at the exact coordinates where wickets fell. Manhattan Chart: Side-by-side grouped bars comparing runs scored per over across both innings. Interactive Over Scrubber: Tapping any over highlights that slice of the match and shows the bowler, runs, wickets, cumulative score, and ball-by-ball sequence. Try It Out / Star on GitHub If you play weekend cricket or enjoy clean, dependency-free web apps, check out the repository: 👉 github.com/ajith-poojari/Cric-scorer Feedback, issues, and stars are hugely appreciated! 
