Work Case study

Museum of Fantasy Sports

A league history platform that treats a fantasy league’s past as an archive worth keeping.

Live
  • Next.js
  • React.js
  • TypeScript
  • PostgreSQL
  • Prisma
  • Auth.js
  • Tailwind CSS
Role
Solo, design and build
When
Aug 2026 to Present
Status
Deployed at /fantasy · real league import waiting on credentials

01

The Problem

Fantasy platforms are built for the season you are in. Last year’s standings are three clicks deep, the year before that is a dead link, and the trade everyone still argues about exists only in a group chat nobody can search.

The interesting part of a long-running league is not this week’s matchup. It is the decade of context around it: who has never won, who wins constantly, which rivalry has a real record behind it and which one is just noise. None of that survives on the platforms themselves.

So the goal was not another stats dashboard. It was somewhere a league’s history is kept deliberately, and served back in a way that means something to whoever is reading it.

02

The Idea

League history deserves an archive, not a leaderboard. Champions, records, rivalries and the moments a spreadsheet forgets, imported from ESPN and Yahoo, and kept permanently.

03

The Build

The frontend never talks to ESPN or Yahoo. Everything is normalised on the way in, so the UI only ever reads one shape.

  1. External platform

    ESPN and Yahoo, each behind its own provider with its own authentication story.

  2. FantasyProvider

    One interface per platform. Adding a third source means writing a provider, not touching the app.

  3. Normalisation

    Platform quirks are resolved here: bench slots, placeholder draft picks, differing season shapes.

  4. Database

    One schema, provider-agnostic. Re-importing a season updates in place rather than duplicating it.

  5. Event engine

    Derives records, awards and memories from stored history instead of storing them by hand.

  6. Personalised UI

    The same row reads differently depending on who is signed in.

04

Technical Decisions

Most of the schema follows from two invariants.

History is append-safe

Mutable rows carry source and lockedFields, and every provider-sourced row has a natural key. Re-importing a season updates in place instead of destroying a manual correction someone made months earlier.

Why it matters: An import that can silently erase human edits is an import nobody will run twice.

A person is not their team

User → TeamMembership → FantasyTeam → Franchise keeps a manager’s identity separate from whatever they named the team that year.

Why it matters: It is what makes career history traversable. Without it, a manager who renames their team every season becomes six different people.

Memories are rendered, not written

A memory stores template + data plus a MemorySubject join, rather than finished prose. One row renders as “You beat Noah” or “Noah beat you” depending on who is reading it.

Records remember their own lineage

Each record keeps previousRecordId, so the app can say a mark stood for three years before it fell, which is the part that actually makes a record interesting.

05

Challenges

Importing real league history is the remaining gate, and it is an access problem rather than a code one. ESPN needs session cookies for private leagues and prior seasons; Yahoo needs a registered developer app for OAuth. Both have to come from the league owner, so the archive fills up as those land.

The two platforms also disagree about shape. ESPN serves the active season and earlier seasons from two different controllers that return different types, and answers 401 rather than 404 for a private season that exists, so a naive fallback would quietly mask a real permission error.

06

Where it is now

The app is built and running: providers, sync, the event engine, auth and the admin surface all work. It is deployed under this domain rather than its own, which is a deliberate constraint: one domain, one certificate, one thing to maintain.

The live build shows the real interface. It does not present invented league history as though it were real.

Framework
Next.js · React.js · TypeScript
Styling
Tailwind CSS · Motion
Data
PostgreSQL · Prisma
Auth
Auth.js, role-based, with an admin surface
Charts
Recharts
Hosting
Netlify, mounted at /fantasy

07

What I Learned

Two things, and they pulled against each other. The first was interface work: an archive is only worth keeping if it is worth reading, so most of the design effort went into making a decade of records legible at a glance rather than dense.

The second was implementing against APIs I did not control. ESPN and Yahoo each return their own shape with their own authentication story, and the interface can only stay simple if that is resolved on the way in. Everything I learned about providers, normalisation and re-runnable syncs came out of keeping that mess away from the UI.