JP
HomeBlogProjectsResumeAbout

© 2026 JP. All rights reserved.

Back to blog

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

AdminJuly 12, 20266 min read
Dx Music League Part 1 : A Fake App That Feels Real

On this page

  • Day one: build the whole app, fake everything
  • Lock the rules before the database exists
  • The design doc: deciding the shape of the data
  • The weird one: YouTube Music before Spotify
  • What existed at the end of the first push

On this page

  • Day one: build the whole app, fake everything
  • Lock the rules before the database exists
  • The design doc: deciding the shape of the data
  • The weird one: YouTube Music before Spotify
  • What existed at the end of the first push
PreviousBuild a Code Editor Like VS Code, Part 7: A Real File Tree and Four More FeaturesNextDx Music League Part 2 : One Backend, Two Worlds

Building 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.

Day one: build the whole app, fake everything

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:

ts
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.

Lock the rules before the database exists

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:

  • Every voter gets a pool of points (default 10) and must spend it exactly — no leaving points on the table.
  • No song may receive more than a per-song cap (default 5) from one voter — you can't just dump your whole pool on your best friend's pick.
  • No voting for your own song (configurable, off by default).
  • Ties break by a fixed algorithm, not a coin flip: most points, then most distinct voters (broad appeal beats one superfan), then title alphabetically. Deterministic, arguable-about, and not a setting.

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 design doc: deciding the shape of the data

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:

  1. Cognito owns identity. The caller's user id is the JWT sub claim, never something in a request body.
  2. The server is the referee. Every game rule is validated server-side; the frontend is just a nice way to fill in forms.
  3. Provider-agnostic everywhere. Submissions store a normalized 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.

The weird one: YouTube Music before Spotify

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.

What existed at the end of the first push

By the end of June 27th:

  • A complete, styled, clickable app — dashboard, league pages, create/join flows, submit, vote, reveal, leaderboard — running entirely on mocks.
  • The game rules locked and documented.
  • A data model and API design doc.
  • A proven read path for the harder of the two music providers.
  • Zero lines of backend code.

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.