Superstar Series
A full-stack esports management platform for competitive Rocket League tournaments, featuring a Discord bot for match reporting, a statistics website, and cloud infrastructure—all built as a polyglot monorepo.
A full-stack esports management platform for competitive Rocket League tournaments, featuring a Discord bot for match reporting, a statistics website, and cloud infrastructure—all built as a polyglot monorepo.
Superstar Series is a competitive Rocket League league organized through Discord. This platform provides end-to-end tournament management:
The system is designed for cost efficiency and real-time performance, using static site generation with nightly builds to minimize database reads while maintaining live update capabilities.
Problem: Firestore charges per document read. With hundreds of users viewing standings and match data daily, real-time queries would generate significant costs.
Solution: Implemented a hybrid static/real-time architecture:
| Approach | Daily Reads | Monthly Cost |
|---|---|---|
| Pure Real-time | ~50,000+ | $15-30+ |
| Static + Nightly | ~2,000 | < $1 |
Problem: Static sites serve stale data, but real-time systems are expensive and slower on initial load.
Solution: Dual-layer data strategy:
Problem: Reporting Rocket League matches requires validating players, mapping platform IDs to Discord users, and ensuring data integrity.
Solution: Multi-step validation pipeline in the Discord bot:
Problem: Tournament data spans Discord (users/roles), Ballchasing (replays), and Firestore (persistence) with complex relationships.
Solution: Shared model library (SuperstarSeries.Firestore.Models) used by both C# and TypeScript projects, ensuring consistent data structures across the entire stack.
The platform is built as a polyglot monorepo combining C# backends with TypeScript frontends:
Why Polyglot?
| Component | Technology |
|---|---|
| Framework | Remora.Discord (C#/.NET 8) |
| Database | Google Cloud Firestore |
| External API | Ballchasing.com |
| Architecture | DI-based services + repositories |
Match Reporting
/report game <ballchasing-link> # Report single game
/report series <ballchasing-group> # Report entire series
Statistics & Analytics
/stats all # Export all season stats (CSV)
/stats user @player # Player-specific statistics
/my_stats # Personal performance data
Clip Management
/clips top5 — View top-rated clips/clips export — Export clip dataConfiguration
/config channel set reports #channel
/config division add @role
/config team add @role <logo-url>
/config bestof set 5
| Category | Technology |
|---|---|
| Framework | SvelteKit 2 + Vite |
| Styling | Tailwind CSS + Skeleton UI |
| Database | Firebase Firestore |
| Hosting | Firebase Hosting (CDN) |
| Visualization | Chart.js |
Build-Time Caching
The SeasonDb class implements efficient data aggregation:
class SeasonDb {
private cachedSeries = new Map<string, SeriesData>();
private namesLookup: Record<string, string> | null = null;
async init() {
// Single pass through Firestore
// All subsequent requests use cached data
}
}
Prerendered API Endpoints
All data is served as static JSON generated during build:
/api/seasons/current.json
/api/seasons/[season]/series.json
/api/seasons/[season]/divisions/[division]/standings.json
/api/seasons/[season]/divisions/[division]/teams/[team]/stats.json
Real-Time Enhancement
After loading static baseline, Firestore listeners subscribe to new data only:
// Only fetches documents created after the static build
onSnapshot(
query(seriesRef, where('ReportedDate', '>', lastBuildTime)),
(snapshot) => mergeNewData(snapshot),
);
| Technology | Purpose |
|---|---|
| Remora.Discord | Discord API framework |
| Google.Cloud.Firestore | Database client |
| Microsoft.Extensions.DI | Dependency injection |
| OneOf | Discriminated unions for error handling |
| Riok.Mapperly | Object mapping |
| FuzzySharp | Fuzzy string matching |
| Polly | HTTP retry policies |
| Technology | Purpose |
|---|---|
| SvelteKit | Full-stack framework |
| Vite | Build tooling |
| Tailwind CSS | Utility-first styling |
| Skeleton UI | Component library |
| Firebase SDK | Auth + Firestore client |
| Chart.js | Data visualization |
| date-fns | Date manipulation |
| Technology | Purpose |
|---|---|
| Firebase Hosting | Static site CDN |
| Firebase Functions | Serverless auth |
| Cloud Firestore | NoSQL database |
| GitHub Actions | CI/CD pipelines |
| pnpm Workspaces | Node.js monorepo |
Rather than choosing between static (fast, cheap) and real-time (fresh, expensive), the platform uses both:
Choosing the right tool for each job:
Complex queries and joins happen once at build time, not on every request:
The C# projects use Result<TValue, TError> types for explicit error handling without exceptions, making failure cases visible in the type system.