Dx Music League Part 1 : A Fake App That Feels Real


Part 1 of a 5-part series on building a self-hosted Music League clone from nothing to a live beta with real users — in about two weeks of evenings.
Every group chat has one: the friend who turns "share a song" into a competition. Music League — submit a song for a theme, everyone votes, points accumulate, someone gets bragging rights — is a perfect game. I wanted a self-hosted version I controlled: my rules, my data, no subscription, and no dependency on someone else's roadmap.
This series is the honest build log. Day one was June 27th. By July 4th it was on the public internet. By the second week it had a real league of eleven players finding real bugs. Here's how it went.
The first commit contains no backend, no database, and no AWS. It contains something more valuable: the entire product, running on mocks.
The frontend is Vite + React + TypeScript, and from the very first day it was organized around three interfaces I started calling seams:
AuthBackend — sign up, sign in, confirm email, reset password. Day one implementation: a mock that stores a fake session in localStorage and signs everyone in as a seeded user named "Curator Max."DataClient — everything the UI knows how to ask for: leagues, rounds, submissions, ballots, results. Day one implementation: an in-memory store seeded with seven leagues and ten rounds spanning every phase of the game.MusicProvider — search a catalog, fetch a track, create a playlist. Day one implementation: a ten-song demo catalog with substring search.Each seam has the same shape: a small TypeScript interface, a mock implementation, and a slot for the real one. The selection rule is absence of configuration means mock:
export const isApiMode = Boolean(import.meta.env.VITE_API_URL);
export const data: DataClient = isApiMode ? new ApiClient() : new MockClient();No env file? You get a complete, clickable, offline product. This one decision paid for itself every single day afterward: every screen in the app was designed, built, and iterated against instant, deterministic fake data. No waiting on deploys, no "the API is broken so frontend work is blocked," no seeding a cloud database to test an empty state.
The mock data wasn't lazy filler, either. Two of the seeded submissions are deliberately tied on points — because I wanted the tie-breaking rule visible in the UI from day one. Which brings me to the most important commit of the day.
Commit four is titled "Lock in voting rules: per-song cap and tie-break." It's a rules decision, not a code feature, and making it early was one of the best calls of the project.
The game's constitution, decided before any backend existed:
Writing these down as rules rather than discovering them as behaviors meant that when the backend arrived, its job description was already written: be the referee. More on that in part 2.
The same day, I wrote docs/data-model-and-api.md — the data model and API design that the backend would later implement. Three principles from that doc survived all the way to production unchanged:
sub claim, never something in a request body.Track snapshot — provider name, native id, title, artists, artwork — never a Spotify-shaped object.The doc also sketched the DynamoDB single-table layout (leagues, memberships, rounds, submissions, ballots, standings, invite codes — one table, seven key patterns). Sketching key design before writing any Dynamo code sounds like over-planning; in practice it's the cheapest time you'll ever have to think about access patterns.
Here's the commit sequence that surprises people: the YouTube Music provider landed on day one — before the backend, before Spotify, before auth.
Why? Because the music-provider seam was the architectural bet of the whole project, and I wanted to prove it against the hard case first. Spotify has a clean official API. YouTube Music doesn't — the community library (ytmusic-api) is an unofficial scraper, Node-only, and CORS-blocked in the browser. If the seam could absorb that, it could absorb anything.
The proof-of-concept script (poc/ytmusic_search.mjs) established the key fact: YouTube Music search works with no authentication at all. No API key, no OAuth, no cookies. And to make it usable during development, I wrote a tiny Vite plugin that runs the scraper inside the dev server, exposing the same /youtube-music/search endpoints a future production proxy would. Search worked end-to-end in npm run dev with zero configuration.
That spike — its findings, its limitations (spoiler: the library can't create playlists), and what it takes to finish the job — gets its own post at the end of this series.
By the end of June 27th:
That last line is the point. The product existed before the infrastructure, which meant every infrastructure decision afterward was made against a real, visible target instead of a guess.
Next: Part 2 — One Backend, Two Worlds, in which the same handler code learns to run on a laptop and on Lambda, and the server becomes the referee.