Project Overview
the13 is a full-stack, real-time football survival prediction game built for a competitive community of Premier League enthusiasts. Players begin each season with 13 points and make weekly predictions across private and public groups, losing points for incorrect picks and being eliminated when they reach zero. The last player standing wins.
The client needed a platform that was engaging and social, technically robust enough to handle live match data, and reliable enough to run autonomously — including automated pick reminders, real-time score updates, and fair, deterministic scoring — without manual intervention.
The Challenge
The client came with a clear vision but a complex set of requirements:
- Real-time gameplay — Pick locking must happen at the exact moment a Premier League fixture kicks off, not minutes later
- Automated lifecycle management — Fixture sync, scoring, and email reminders all had to run on schedule without human input
- Fairness at scale — Scoring rules (team usage limits, mid-season group creation exemptions, tiebreakers) needed to be enforced precisely across all groups simultaneously
- Multi-group participation — A single user can belong to multiple groups with independent leaderboards, picks, and scoring histories
- Admin observability — The client needed real-time visibility into background service health, fixture sync status, and email delivery without writing a single SQL query
Technical Solution
Architecture
We built the13 as a monolithic Blazor Server application on .NET 10, structured into four clean layers:
| Layer | Project | Responsibility |
|---|---|---|
| Presentation | The13.UI | Blazor Server components, admin console, authentication flows |
| Business Logic | The13.Services | Background services, scoring engine, email/SMS, API integration |
| Data Access | The13.Data | EF Core 10, repositories, migrations |
| Domain | The13.Contracts | Entities, enums, interfaces, DTOs |
Why Blazor Server? The interactive, real-time nature of the app made Blazor Server the right choice. Live match scores push to every connected user simultaneously via SignalR — no client-side polling, no page refreshes, no WASM payload. This keeps the experience fast and the server fully in control of state.
Real-Time Fixture Pipeline
Three background services run continuously in production, each with a distinct responsibility:
DailyFixtureSyncService — Fires at 13:45 UTC every day — well before Saturday's typical 15:00 UTC kickoffs — to pull fixture schedules, kickoff times, and any changes (postponements, time adjustments) from API-Football for a rolling ±14 day window. This ensures players always see accurate upcoming fixtures.
LiveMatchWatcherService — Dynamically adjusts its polling frequency based on match activity:
- Idle (no fixtures within 2 hours): polls every 5 minutes
- Pre-kick (fixtures within 2 hours): polls every 120 seconds
- Live (fixtures in progress): polls every 45 seconds
When a fixture kicks off, this service locks all associated picks within seconds — a critical fairness requirement. When a fixture concludes, it marks it IsClosed = true, triggering the scoring pipeline.
PickReminderHostedService — Scans every 5 minutes for upcoming rounds and sends consolidated reminder emails to users who haven't submitted picks, approximately 24 hours before the first kickoff. Key design decisions:
- Reminders are gated: they only send if the previous round is fully closed, preventing premature notifications
- Deduplication is enforced via a
PickReminderLogtable with a unique constraint on{UserId, Season, Round}— each user receives exactly one reminder per round, even if the service scans multiple times during the window - Reminder emails are consolidated: if a user belongs to three groups and is missing picks in all three, they receive a single email listing all three groups
Scoring Engine
The scoring model is deceptively simple to players but precise to implement:
Player Score = 13 - Σ(PointLoss per Round)
Win → 0 points lost
Draw → −1 point
Loss → −3 points
No Pick → −3 points
Tiebreakers use cumulative goal differential (picked team goals minus opponent goals, summed across all rounds). Scores are computed per-group, per-round, per-user and stored in GroupRoundScore for fast leaderboard queries.
Two fairness rules required careful implementation:
- Team usage limits — A user may pick the same team no more than twice per season. The UI enforces this in real time by counting picks in
FixturePickbefore rendering the pick interface. - Mid-season group creation exemption — If a group is created after a round has already started, members are never penalized for that round. The scoring service compares
Round.FirstKickoffUtcagainstGroup.CreatedAtbefore applying NoPick penalties.
Authentication & Identity
The app uses ASP.NET Core Identity with multiple authentication paths:
- Email/password registration with email confirmation gating
- OAuth 2.0 external login providers (Google, Microsoft)
- TOTP-based two-factor authentication via authenticator apps
- Twilio Verify for SMS-based phone number confirmation
- WebAuthn/passkey infrastructure prepared for future passwordless login
Admin Observability Console
A dedicated admin console at /admin/* gives the client full operational visibility without database access:
- Log Viewer — Real-time in-memory Serilog sink displaying the last 30 minutes of log events, filterable by severity and keyword with auto-refresh
- Activity Report — Permanent structured audit trail in the database, searchable by category, severity, and correlation ID for tracing multi-step operations end to end
- Test Reminder System — Manually select users, target a round, and trigger the reminder flow using the same production code path — invaluable for SMTP configuration verification and email content testing
- User & Group Management — Search, inspect, and delete users or groups with cascading cleanup of all related records
- Documentation Viewer — Markdown-based admin documentation rendered in-app via a custom
Pulse.DocsRazor Class Library, with full-text search and 24-hour HTML caching via ASP.NET Core'sHybridCache
Key Outcomes
| Pick locking latency | Picks lock within 45 seconds of kickoff via live polling |
| Reminder delivery | Consolidated, deduplicated emails delivered within 5 minutes of the reminder window opening |
| Scoring accuracy | Deterministic scoring across all groups simultaneously once the final fixture in a round closes |
| Zero-touch operation | All fixture sync, scoring, and reminders run autonomously in production |
| Admin self-service | Full operational visibility without database access or developer involvement |
| Multi-group fairness | Team usage limits and group creation exemptions enforced consistently across all groups |
Technology Stack
| Framework | ASP.NET Core / Blazor Server (.NET 10) |
| Real-time UI | SignalR WebSockets |
| Database | SQL Server via Entity Framework Core 10 |
| Authentication | ASP.NET Core Identity, OAuth 2.0, TOTP 2FA |
| SMS Verification | Twilio Verify API |
| MailKit / MimeKit over SMTP | |
| Fixture Data | API-Football REST API |
| Logging | Serilog (file, console, in-memory sinks) |
| Caching | ASP.NET Core HybridCache |
| Documentation | Markdig (Markdown → HTML), custom Razor Class Library |
| Background Services | ASP.NET Core IHostedService |
What Made This Project Interesting
The constraint that drives strategy. The two-picks-per-team rule is a single database constraint (UNIQUE index on {GroupId, UserId, Team} usage count), but it fundamentally changes how players engage with the game. It was important that the UI surface this information clearly — showing exactly which teams are available, which are used once, and which are exhausted — so the rule felt like a feature rather than a limitation.
Fairness is code, not policy. Every fairness rule in the game is enforced in code: team limits, mid-season exemptions, reminder deduplication, pick locking precision. There are no edge cases where an admin needs to manually intervene to keep the game fair. This was a deliberate design goal and required careful modeling of the PickReminderLog, GroupRoundScore, and FixturePick entities.
Observability as a first-class feature. The admin console wasn't an afterthought. Structured activity logging with correlation IDs, in-memory log streaming, and a purpose-built test harness for the reminder system mean the client can diagnose and resolve most operational issues independently — exactly what a platform running unsupervised at 45-second intervals needs.