Back to work

Brookside Wiffle

Full-stack league management platform

10 min read
Brookside Wiffle

Brookside Athletics Wiffle Ball League Platform

A full-stack league management platform built to replace an outdated third-party service with a modern, fast, purpose-built system, covering everything from public-facing stats and standings to a complete admin dashboard for game-day operations.


The Origin Story

It started with an invitation to join a men's wiffle ball league through a coworker. The league — Brookside Athletics Wiffle Ball (BAWL) — was using LeagueLineup, a legacy hosted platform from Stack Sports. While functional, the experience was dated: slow load times, rigid layouts, a design language stuck in the early 2000s, and limited flexibility for how stats and content were presented.

Here's what the league was working with:

The Old Site - LeagueLineup

Home page — a basic welcome page with a news block, venue status widget, and headline sidebar. No standings preview, no leader highlights, no featured content.

LeagueLineup home pageLeagueLineup home page

Standings — a plain HTML table with wins, losses, ties, percentage, and point totals. Functional, but no streak tracking, no differential context, and no visual hierarchy.

LeagueLineup standingsLeagueLineup standings

Schedule — a flat table listing every game across the season with date, time, score, teams, and venue. No status-aware presentation, no filtering, no expandable detail.

LeagueLineup scheduleLeagueLineup schedule

Team stats — a dense table showing batting statistics per player. The data is there, but the presentation is raw and the navigation between offense, defense, and pitching is tab-based with no visual refinement.

LeagueLineup team stats viewLeagueLineup team stats view

The biggest issue wasn't just the look — it was the lack of control. LeagueLineup dictated the layout, the stat fields, the navigation, and the content model. There was no admin workflow designed for wiffle ball specifically, no way to feature video content, no calculated standings tied to game results, and no modern tooling for the people running the league day-to-day.

I saw an opportunity to build something better from scratch.


What I Built

The Brookside Wiffle Ball League Platform is a season-aware, dual-surface web application with two major areas:

  1. A public website for players, families, fans, and visitors — with schedules, results, standings, team pages, player pages, news, videos, and league information.
  2. An admin dashboard for league operators — with full CRUD for teams, rosters, games, stats, news, videos, and home page content management.

The core principle driving the architecture: completed game data is the single source of truth. Standings, records, leaderboards, and cumulative player stats are all calculated from stored game outcomes and per-game stat rows — never manually maintained totals that could drift out of sync.


Public Website

Home Page

The home page acts as both a dashboard and a marketing surface. It answers the most common visitor questions immediately: who's leading the league, who's performing well, what's happening next, and what new content has been published.

It combines league branding with operationally important modules: a standings preview, league leaders, a next-event card, featured video content, recent news, and current weather conditions at the field.

Public home pagePublic home page

Teams Directory

The teams directory shows season-filtered team cards linking into full team pages. Each card displays the team identity, coaching information, brand treatment through the team logo (or fallback initial-based badge respecting team color), and a season record — all calculated from actual game results.

Teams directoryTeams directory

Team Detail

The team detail page is a season-aware summary combining team identity, coach name, calculated record, full roster with derived batting summaries, and schedule context in one view. Visitors can quickly understand player contributions without navigating into each profile individually.

This page is one of the clearest examples of the system pairing manually maintained metadata (coach, logo, colors) with computed data derived from actual games and stat entries.

Team detail pageTeam detail page

Player Detail

Player profiles are organized into focused tabs: cumulative offensive performance, pitching output, and a per-game log.

The pitching tab handles missing data defensively — if a player has no pitching data, the interface hides that tab rather than presenting misleading zeros as if the player had actively pitched. This keeps player pages trustworthy.

Schedule

The schedule organizes games by date and status for the selected season, supporting multiple lifecycle states: scheduled, live, final, postponed, and cancelled. It works for both pre-game planning and post-game reference from the same underlying data.

Public schedulePublic schedule

Results & Box Scores

The results page goes beyond listing winners and losers. It supports expandable box scores so visitors can inspect actual player performance lines behind each final result — turning the results page into a detailed archive rather than a thin score ticker.

Player stat rows are deduplicated for display so a player appears exactly once in the rendered view, keeping public stats clean and readable.

Standings

Standings are entirely calculated from final games — not entered manually. The page surfaces wins, losses, ties, games played, winning percentage, runs for, runs against, and run differential. The public page also adds short-horizon context like last-ten form and current streak, computed from chronological game history.

If a final score changes, standings update automatically. There's no separate manual standings workflow to maintain.

Calculated standings pageCalculated standings page

Videos

The videos page is built around YouTube-backed media. URLs are normalized to canonical embed format, thumbnails can be derived from the YouTube video ID, and playback happens through embedded presentation rather than pushing users to raw external links.

Videos can be marked as featured to appear prominently on the home page alongside livestream and highlight content.

Videos pageVideos page

About

The about page rounds out the public surface with league description, community-facing context, and a path to the admin area. Together with news and videos, it makes the site more than a stats system — it's a league communications hub.


Admin Dashboard

Authentication & Security

Admin access uses a single configured password with no self-service account creation — intentionally simple for a small-league operation. Under the hood, security is layered:

  • Redis-backed sessions with HttpOnly, SameSite=Strict, Secure cookies and a 12-hour TTL
  • Brute-force protection via Redis-backed failed-login counters with configurable attempt limits, failure windows, and lockout durations
  • Fail-closed rate limiting on the login endpoint — if the rate-limit backend is unavailable, login traffic is blocked rather than exposed to unthrottled abuse
  • Admin data freshness — admin reads force no-store cache headers so operators always see the latest saved values

Home Management

Controls the public next-event module. The backing table is enforced as a singleton (database check requiring id = 1), so the home page always reads from one authoritative record instead of juggling a list of event candidates.

Schedule Management

Full CRUD for the games table: date, time, home/away teams, venue, status, and final-game summary fields (scores, hits, errors, recap text). The data model enforces meaningful rules — home and away teams must differ, scores must be non-negative, and final games require both scores to be present.

Admin schedule managementAdmin schedule management

Team & Roster Management

Team management covers name, coach, logo, primary color, schedule link, and season. Logo uploads go through an S3-backed flow with automatic cleanup of old assets when logos are replaced or teams are deleted.

The team portal provides player CRUD for each roster: name, number, position, team association, and season alignment validation.

Admin team management overviewAdmin team management overview

Admin team portal — roster managementAdmin team portal — roster management

Game Stats Entry

This is the most operationally sensitive area. The workflow supports two paths:

  • Quick score — fast updates to home/away scores and status without a full box score
  • Full entry — complete player-by-player stat entry grouped by team

Full entry loads existing stat rows for the game so admins edit current truth rather than working blind. Each player has at most one stat line per game (enforced by a unique (gameId, playerId) constraint).

Batting inputs: AB, R, H, 2B, 3B, HR, RBI, BB, K Pitching inputs: IP, H, R, ER, BB, K, plus W/L/SV flags

On save, the system upserts rows when a player has data, and deletes rows when all editable fields are zeroed out — preventing abandoned blank lines from polluting the stat ledger. Derived values like battingGp, battingPa, and normalized pitchingR ≥ pitchingEr are handled automatically.

Game stats browseGame stats browse

Full game stats entry — game metadataFull game stats entry — game metadata

News & Video Management

News supports full CRUD: date, title, summary, and content. Video management supports CRUD around title, thumbnail, date, duration, views, URL, featured state, and type — with built-in YouTube URL normalization and thumbnail derivation.

Admin news managementAdmin news management

Admin video managementAdmin video management


Statistics & Business Logic

Core Data Model

The statistics system is built around the relationship between teams, players, games, and game_player_stats. All four are season-scoped. Several domain-consistency rules are enforced at the server layer:

  • Player season must match team season
  • Game season must match both participating teams
  • Stat row season must match the game, player, and team
  • Stat row team must be one of the two teams that participated in the game

Batting

Storage-level fields: GP, PA, AB, R, H, 2B, 3B, HR, RBI, BB, K, SB, CS, HBP, SF

Validation rules: Hits ≥ 2B + 3B + HR. At-bats ≥ hits. Plate appearances ≥ AB + BB + HBP + SF.

Derived calculations: AVG, OBP, SLG, OPS — with OBP supporting the full HBP/SF-aware formula when those inputs exist. Singles derived as H − 2B − 3B − HR.

Pitching

Storage-level fields: GP, GS, IP, H, R, ER, BB, K, HR, W, L, S, HLD, CG

Key behavior: ER cannot exceed R (normalized on write). Innings pitched uses baseball-style fractional notation (.1 and .2 represent outs, not tenths). ERA is a calculated derivative of earned runs and outs recorded — never a manually entered field.

Validation Architecture

Validation exists in three layers:

  1. Database constraints — non-negative numeric rules, final-game score completeness, unique indexes, foreign key relationships
  2. Schema validation — catches malformed or impossible payloads before they hit storage
  3. Route-level domain checks — cross-record logic like season alignment and team-game participant verification

Tech Stack

Frontend

LayerTechnology
FrameworkReact 19 + TypeScript
Build toolVite
RoutingWouter
Data fetchingTanStack Query
StylingTailwind CSS v4 + shadcn/Radix UI primitives

Season selection is managed through a dedicated React context so all pages react to the same chosen season. Shared components like the layout shell and team-logo component keep league branding consistent across the entire site.

Backend

LayerTechnology
RuntimeNode.js + Express 5 + TypeScript
ORMDrizzle
DatabasePostgreSQL (Neon)
Cache/SessionsRedis (Upstash)
Media storageAWS S3 (presigned uploads)
DeploymentVercel (serverless Node function)

The API exposes REST-style JSON endpoints with explicit cache profiles for public reads (s-maxage + stale-while-revalidate). Different read classes use different cache windows, and admin reads bypass caching entirely.

Infrastructure & Security

  • Content Security Policy with configurable off/report-only/enforce modes
  • Rate limiting — fail-open for public reads (availability-first), fail-closed for writes, uploads, and admin login (security-first)
  • Health checks/api/healthz for process liveness, /api/readyz for full service readiness (verifies database + Redis)
  • Request IDs attached to all API handling for log correlation
  • Serverless-optimized connection pooling with tunable max pool, idle timeout, and connect timeout

API Surface

The application exposes a comprehensive REST API covering:

  • Operational: health checks, seasons, weather
  • Home content: summary endpoint, next-event CRUD
  • Teams: full CRUD + calculated records + team summaries
  • Players: full CRUD + calculated stats + leaderboards + player summaries
  • Games: full CRUD with status normalization
  • Standings: calculated from game ledger (read-only)
  • News & Videos: full CRUD with YouTube URL normalization
  • Game Stats: individual and bulk stat entry with upsert/delete-on-zero semantics
  • Admin Auth: login, session inspection, logout
  • Uploads: S3 presigned URL workflow with managed asset cleanup

Key Design Decisions

Calculated over manual. Standings, records, and leaderboards are derived from game data at read time. There's no second source of truth that can drift.

Season-scoped domain model. Teams, players, games, and stats are partitioned by season. Historical seasons are retained without duplicating the site or hardcoding separate builds.

Defensive UI patterns. Missing data is hidden, not disguised as zeros. Pitching tabs don't appear for non-pitchers. Empty stat lines are deleted, not saved as blanks.

Layered security for a small operation. Single-password auth is appropriate for the league's scale, but it's backed by Redis sessions, brute-force lockout, fail-closed rate limiting, and CSP headers — not left as a bare password check.

Content hub, not just stats. News, videos, next-event cards, and weather make the site useful beyond pure statistics tracking. The YouTube integration and featured video system turn the site into a media destination for the league community.

Next project

PromptVault

Prompt Engineering and Operations Platform