RCS League: A Full Season-Management Platform for a Recovery Softball Community

How we built RCSLeague.com — a full-stack league platform with multi-season scheduling, auto-calculated standings, playoff brackets, and a public site, live for 500+ players. From data model to deploy.

The Ask

A recovery-community softball league had outgrown its tools.

Scheduling lived in spreadsheets. Standings were updated by hand and were usually a week behind. Players found out about rainouts through group texts. Past seasons existed only in whoever's memory was sharpest. For a league of 500+ players across multiple divisions, "a spreadsheet and a group chat" had quietly become a part-time job for the organizers — and a frustrating experience for everyone trying to find out when and where they were playing.

They didn't need a website. They needed a system. The website is just the part the players see.

Why This Was a Real Software Project

It's easy to look at a league site and think "schedule, standings, a few team pages — that's a brochure with tables." It isn't.

A league is a small data product with real rules. Teams belong to divisions; divisions belong to seasons; games produce results; results change standings; standings seed playoffs; playoffs advance winners; and all of it has to survive into next season as history without getting tangled with next season's live data. Get the data model wrong and every feature on top of it fights you forever.

So before writing a single page, we spent our time on the part nobody sees: how to model a league that runs year after year.

The Data Model That Made It Work

The key decision was separating franchises from teams.

A franchise is a persistent identity — "the Renegades" — that exists across every season. A team is that franchise's instance in one specific season and division: the Renegades in the 2026 Summer division. Games, standings, and rosters attach to the season-specific team. The franchise just carries the name, the logo, and the through-line.

That one distinction is what makes the rest of the system clean:

  • A player can favorite a franchise and follow it season to season, even as divisions and rosters change.
  • Standings and schedules are always scoped to a season, so archiving last year is trivial — the data was never commingled in the first place.
  • Division champions, head-to-head history, and logos all hang off stable identities instead of being re-typed every spring.

The full schema runs to 20 tables — seasons, divisions, franchises, teams, games, game results, standings, brackets, bracket games, locations, media albums/photos/videos, announcements, rules, board members, contact submissions, a homepage carousel, and site settings. But the franchise/team split is the load-bearing idea everything else rests on.

What We Built

The public site (no login needed) is what players, families, and fans use:

  • Game schedule with results, filterable and paginated
  • Live standings by division
  • Team pages and rosters
  • Photo and video galleries by album
  • League rules, locations with embedded maps, announcements, and a contact form

The admin and commissioner tools are where the league actually runs. We built two privilege levels — admins get everything (users, site settings, every entity), commissioners get day-to-day operations (seasons, divisions, teams, scheduling, scores, standings, content). The split matters: the people running game day shouldn't need, or have, the keys to user management and site configuration.

Registered players get a lighter tier — pick a favorite franchise and the home page becomes personal: your team's record, your next games, your place in the standings.

The Scheduling Engine

Hand-building a season schedule is the worst job in running a league. So we automated it.

Commissioners define scheduling rules per season — game duration, whether to allow doubleheaders, the default day of week, which fields and locations are in play — and can override them per division or per team. The bulk scheduler then generates a full slate of games against those constraints, which the commissioner reviews and adjusts before publishing. Fields are numbered, byes are handled explicitly, and games carry a real status (scheduled, completed, cancelled, postponed, bye) instead of being silently deleted when plans change.

The goal was simple: turn an afternoon of spreadsheet wrangling into a few minutes of "generate, review, publish."

Standings That Take Care of Themselves

This is the feature that quietly killed the spreadsheet.

When a commissioner enters a game result, the standings update themselves. Recording a final score writes the win, loss, or tie to both teams' records in the same operation — no separate "now go update the standings table" step, no drift between what the schedule says happened and what the standings claim. For the rare edge case (a forfeit, a correction, a manually adjusted record), there's an explicit manual-override flag so a human can take the wheel without the system fighting back.

The result: standings are correct the moment a score is entered, and they're correct for everyone at once.

Playoff Brackets

When the season ends, the league runs playoffs — and brackets are their own kind of hard. We supported three formats: single elimination, double elimination, and three-game guarantee. The bracket generator takes a seeded list of teams and builds the entire structure up front, wiring each game to the games that feed it ("the winner of game 2 plays here, the loser drops to here"). As real results come in, winners advance automatically and byes resolve themselves for any bracket size.

We didn't build this from nothing — the bracket engine was adapted from a tournament app we'd already written, stripped down to just the structural logic. Reusing hard-won code from a past project is exactly the kind of leverage that lets a studio ship a feature like this without it eating the whole timeline.

The Unglamorous, Important Parts

A platform that real people log into has to be careful about the boring things:

  • Media uploads are validated in layers — file extension, MIME type, and magic-byte inspection — then processed server-side with Sharp, which resizes images, generates square thumbnails for the gallery, and strips EXIF metadata so nobody's location data ships with a game-day photo.
  • Authorization is enforced on the server, not just hidden in the UI. The frontend hides buttons by role for a clean experience, but the API independently checks the caller's role on every protected operation. Hiding a button is UX; checking the token is security.
  • Abuse protection — rate limiting on the API, a honeypot on the contact form, and Postgres row-level security as a backstop.

None of this is visible to a player. All of it is what separates a real product from a prototype.

The Stack

The whole thing runs on free-tier-friendly infrastructure, deliberately:

  • Frontend: React 19 + TypeScript, built with Vite, routed with React Router, styled with Tailwind — deployed on Vercel.
  • Backend: a TypeScript Express API on Railway.
  • Database & auth: PostgreSQL and email/password authentication via Supabase.
  • Testing: Playwright end-to-end coverage across the public pages, auth flows, account management, and the admin scheduler.

That trio — Vercel, Railway, Supabase — is our go-to for an app that's more than a static site but shouldn't carry AWS-level complexity or cost. A community league shouldn't be paying enterprise hosting bills to publish a schedule, and with this stack it doesn't.

What This Project Showed Us

RCS League looks like a sports website. Underneath, it's the full span of what we do as a studio:

  • Data modeling that has to hold up across years, not just a demo
  • Backend engineering with role-based authorization and rules-driven automation
  • Real product features — a scheduling engine, auto-calculating standings, a bracket system — not just CRUD forms
  • Frontend that serves three different audiences (the public, players, and administrators) from one codebase
  • Security and operations done quietly and correctly
  • Reuse — leaning on code we'd already battle-tested to ship more, faster

It's live, it's run by the league's own organizers, and it's serving 500+ players a season. The spreadsheet is retired.

Have a League — or Anything Like One?

The hardest problems here weren't unique to softball. Persistent identities across recurring cycles, rules-based scheduling, results that ripple into standings, an archive that doesn't pollute live data — that shape shows up in tournaments, class schedules, equipment bookings, volunteer rosters, and a dozen other places where an organization has outgrown its spreadsheet.

If that sounds like something you're wrestling with, let's talk about it.

Stay in the loop

Dev tool reviews, business tips, and web insights. No spam.