How the league ran
The league ran in Discord and Google Sheets. Match results came from exported replay data and player submissions, staff approved or denied them, and other staff built and maintained the sheets. The platform connected all of it through one API: the Discord bot sent staff and team actions to it, sheet data was imported into it, and the website read from it.
The website



How the system was set up
Select a part to see what it did and how it connected.
Technical decisions
Every route needed a valid token unless it was marked @Public(), and only login, token refresh and the health check were. A new endpoint was protected automatically. The API also checked roles, so the website’s key could only read, and each token carried a version number, so logging out invalidated all of that account’s tokens.
Discord IDs are 64-bit numbers. JavaScript rounds numbers above 2⁵³, and a rounded ID points to the wrong record. The API only accepted IDs as strings of digits, queries returned ID columns as text, and a global interceptor converted any BigInt values to strings.
Results and stats came in as bulk imports. Each import used INSERT … ON DUPLICATE KEY UPDATE, so sending the same week again updated the existing rows instead of creating duplicates.
Roster changes with several steps ran in a transaction. For a captain swap, the API demoted the old captain, promoted the new one, then checked the team had exactly one captain. If not, the whole change was rolled back.

Moving from Express to NestJS
The API started out in Express and was rebuilt in NestJS with the same 54 routes, so the bot and website could switch over without changes. The rebuild split the code into modules by domain so it could grow, added validation and types to every request, and tightened authentication. Before launch, the new API ran in dev on the current season’s data alongside the Express API, and it replaced Express between seasons. An AI agent (Codex) wrote the tests with human guidance, and every issue it found was checked by hand before it was fixed.
Keeping the site up after shutdown
When the platform shut down, the API and database went offline but the site stayed up. I saved the API’s responses and removed anything that linked a player’s name to an outside account, like tracker links with Steam and Epic IDs and Discord avatars with user IDs. I also copied the team and sponsor logos off the CDN. The site’s server routes now serve that saved data in the same format the API used, so no pages or components had to change. That’s the live demo linked above.

What I’d do differently
A lot of the league’s work happened outside the platform, in the Discord bot and Google Sheets. On Car Soccer City I’m building those tools into the platform itself, with admin screens in the web app and API endpoints to support them.
