When you open Scorebook on your phone, you’re using a web application. The screens, scoring logic, saved matches, live updates, and even these articles are supported by several different technologies working together.
As a software engineer, I care about how those pieces fit. As someone who keeps the book, I care about what they let me do.
The choices behind Scorebook need to support both. I want a system we can maintain and improve, and I want the person at the table to have an easy time using it.
Here’s what we’re using and where each piece earns its place.
A web app you can put on your home screen
Scorebook is a Progressive Web App, usually shortened to PWA.
You can open it through a web address. On supported devices and browsers, you can also install it on your home screen and open it in its own app window. That gives it a familiar app experience without requiring us to maintain separate iPhone and Android applications. About progressive web apps
That approach fits the way I want volunteers to access Scorebook. Someone can open a match invitation on the phone they already have. A regular user can keep the app on their home screen. The same application can also run on a tablet or computer.
It means we can put our development effort into one shared product. We still have to test different screen sizes and browser behavior, but we aren’t building every scoring feature twice for two different mobile platforms.
A PWA also gives us tools for handling unreliable connections. That matters in a gym, where having Wi-Fi available and having dependable Wi-Fi are two different things.
Offline support takes more than installing the app
Scorebook uses Angular’s service worker to keep application files on the device. A service worker is a browser feature that can respond with a saved copy of those files when the network is unavailable. It also helps manage new versions of the application. Angular’s service worker documentation
But opening the app offline is only part of the problem. We also need to think about the work someone enters.
Scorebook keeps the active scoring session and its Undo history in browser session storage. That helps preserve the current tab’s work through a refresh. The libero tracker uses another browser storage system, IndexedDB, to hold pending actions while they wait to be sent.
Those pending libero actions retain their identity when retried. If the server saved a replacement but the response never reached the phone, retrying should confirm that same action rather than record another replacement.
There are limits. A device can’t send live updates to another device without a connection. A match that has never been loaded isn’t automatically available offline. And local storage isn’t a substitute for a confirmed save to the server.
That’s why we test connection loss and recovery as their own workflows. Calling something a PWA doesn’t solve those problems for us.
Angular gives the application its structure
Angular is the framework behind Scorebook’s interactive screens.
Live Score, Game Setup, the Scoresheet, Libero Tracker, and Match History are built from components. A component is a piece of the interface with a defined job. Some are full screens; others are shared controls used in several places.
That structure is useful in an app where the same match appears in different forms.
The scorekeeper needs fast controls. The libero tracker needs a focused replacement record. Someone reviewing the scoresheet needs the detailed notation. Those views need to stay connected to the right information without each inventing its own version of the match.
Shared components also help us improve the interface consistently. When we refine the number picker, we can carry that improvement into the places that use it rather than maintaining several unrelated keypads.
Angular brings a substantial amount of structure. It takes work to learn and maintain, but Scorebook has enough connected screens and responsibilities to benefit from it.
Most of the courtside interface is custom. We use Bootstrap for selected interface pieces, such as dialogs and popovers, while the scoring controls and phone layouts are tailored to the job.
TypeScript helps us catch mistakes before opening the app
The frontend is written in TypeScript, which adds checks to JavaScript.
We describe what a match, game, lineup, or substitution should contain. When we change one of those definitions, TypeScript can identify code that no longer matches it.
For example, if a screen expects a player number but a change supplies something else, we want to find that during development.
That doesn’t prove the volleyball behavior is correct. A perfectly valid number can still belong to the wrong player. TypeScript helps catch one category of mistake; rules checks and tests handle others.
We also use RxJS to distribute certain events inside the application. When match information changes or a notification needs to appear, interested parts of the interface can respond. It helps coordinate updates without requiring every screen to constantly check whether something happened.
Node.js and Express handle the server’s work
The application on your phone connects to a server running Node.js and Express.
Node.js runs our server code. Express organizes the requests it receives, such as opening a match, saving changes, claiming an invitation, or emailing a scoresheet.
This is also where access is enforced.
Hiding a button on a screen isn’t enough to protect a match. The server needs to check who is making the request, which organization or match they can access, and whether their role permits the requested action.
A viewer should be able to see the match without changing it. A referee needs a different set of permissions from a scorekeeper. A libero tracker needs to maintain the libero record without gaining general access to rewrite the match.
The server checks those boundaries independently of what the interface displays.
The backend is written in JavaScript, while the frontend uses TypeScript. They share a language family, but information arriving at the server still needs its own validation.
PostgreSQL keeps the shared record
PostgreSQL is the database behind Scorebook.
It stores organizations, memberships, seasons, rosters, and matches. Those records have relationships that matter. A match belongs to an organization. A roster belongs to a season. A person’s access depends on their membership.
PostgreSQL also lets us store the detailed match record in a flexible document format called JSONB. That accommodates the nested information inside a match, including games, lineups, rallies, and logs.
As the app grew, we separated the libero tracker’s stored record from the scorekeeper’s broader match record. That helps prevent one person’s save from overwriting work that belongs to the other role.
The database supports the tools needed to coordinate those writes, but we still have to design the ownership rules correctly. Choosing a database doesn’t automatically make simultaneous editing safe.
WebSockets keep the other devices informed
When the scorekeeper records a point, other connected devices need to hear about it.
Scorebook uses WebSockets, through a library called ws, to maintain a connection between the browser and server. The server can send an update when the match changes instead of waiting for everyone to refresh.
That supports the shared score-table experience. The scorekeeper can work on a phone while someone else watches the scoresheet on a tablet.
The connection also needs recovery behavior. Devices disconnect. Messages can arrive late. A phone can return with an older view of the match.
We track revisions so an older update doesn’t simply replace newer information. When there’s a conflict the app can’t safely resolve, it needs to make that visible.
The live connection carries the updates. The saving and conflict-handling logic determines what should be accepted.
Invitations, email, and the finished scoresheet
Several smaller technologies support features that make the app practical to use.
A QR-code library turns a match invitation into something a volunteer can scan. The QR code carries the link; our server validates the invitation and establishes the protected, match-specific session.
For organization accounts, Scorebook supports email verification codes and Google Sign-In for provisioned users. Volunteers joining through match invitations follow a separate path, so they don’t need an organization account just to help at the table.
Mailgun handles email delivery, including login codes, invitations, and scoresheet attachments.
We also have a Twilio integration for text-message invitations, although SMS invitations are currently disabled. Having an integration in the code and having a feature available to users are separate things.
For emailed scoresheets, html2canvas captures the rendered sheet and jsPDF assembles the PDF. That lets us produce a document from the scoresheet the application has already prepared. Browser printing has its own layout rules because fitting a physical page introduces different constraints.
These are fairly small pieces of the overall stack, but they connect the application to what people need before and after a match.
Heroku runs the application
Scorebook is hosted on Heroku, a managed platform that runs the server application and provides the environment around it. How Heroku works
We have separate staging and production applications, with separate databases.
Staging gives us somewhere to verify changes before they reach the live product. Production is the version people actually use.
For a small team, managed hosting reduces the amount of infrastructure we need to operate ourselves. We can spend more time improving Scorebook while still taking responsibility for configuration, database changes, monitoring, and releases.
There is an ongoing hosting cost, and we work within the platform’s capabilities. That’s part of the tradeoff. The convenience is valuable at the scale we’re operating today.
GitHub connects the development and release process
Git records changes to the code. GitHub gives us the shared repository, tickets, project board, pull requests, and review history.
GitHub Actions runs the automated checks.
When we propose a change, the workflow checks the code, builds the application, and runs the test suites. Independent work runs in parallel where that makes sense. The browser tests share a prepared build, and the larger integration suite is divided between isolated test environments.
The testing tools have different jobs:
- Vitest checks individual frontend behaviors quickly.
- Node’s built-in test runner checks server-side behavior.
- Playwright opens real browsers and exercises workflows.
- Disposable PostgreSQL databases let integration tests use real storage without touching anyone’s actual matches.
Some browser tests check the interface in isolation. Others exercise the real server, authentication, database, and live connections together. We need both kinds of evidence.
After independent review and my approval, the deployment process verifies the version being released. Heroku waits for the required checks, and another check confirms that the deployed application responds.
That’s our CI/CD process in practical terms: repeatable testing and a controlled route from an approved change to a running application.
Insights uses a simpler publishing approach
These articles are part of the same repository, but they don’t need to behave like the scoring interface.
Each article is a Markdown file, which is plain text with simple formatting. It also contains publication information such as the title, author, date, summary, and status.
Two small libraries handle it. gray-matter reads the publication information, and markdown-it turns the article into a web page.
The server sends the complete article when someone opens it. Readers, search engines, and social-preview services receive the text and metadata without first having to run the interactive Angular application.
We generate the article listings, search results, RSS feed, and sitemap from those same files.
That lets us use the review and release process we already have. We don’t need a separate content-management service, another editorial database, or another set of accounts.
There’s a tradeoff: publishing goes through the repository rather than a dedicated browser-based writing tool. For the way I’m working with Claude and Codex, that fits well.
We also explicitly keep Insights outside the PWA’s cached application navigation. Otherwise, the browser could serve the scoring application when someone was trying to open an article. Even the publishing section needs to fit correctly with the rest of the system.
What I want from this stack
These choices give us a shared application across devices, a place to keep the match record, a way to coordinate people working together, and a process for checking changes before release.
They also leave us responsible for the details. Angular doesn’t know volleyball. PostgreSQL doesn’t decide who should be allowed to score. A PWA doesn’t automatically preserve every action through every connection failure. Those are things we have to design, implement, and verify.
That’s where most of the work goes.
When I evaluate the technology behind Scorebook, I’m thinking about whether we can keep improving the product without making it harder to operate or harder to trust. I want us to be able to fix an awkward interaction, add a useful capability, and check the result thoroughly.
The person keeping the book shouldn’t need to know which library made that possible. They should be able to open Scorebook on the device they brought and get on with the match.