Spent a real debugging session chasing a "not matching configuration" error assuming it was host mode, when it was actually guest PKCE mode hitting a redirect URI (production, root path) that was simply never registered — the README only documented the two local dev URIs, not that host mode and guest mode each need their own entry per environment (four total), and gave no way to tell which flow's request was actually failing. Added the full four-URI table and a note to check the browser's actual address bar at the point of failure — the error message is identical regardless of which flow's redirect_uri didn't match.
5.1 KiB
fullhouse
Music bingo, generated from a real Spotify playlist. Based on bingo-bango by f8al (MIT licensed) — same core idea (a playlist becomes bingo cards, a square is either an exact song title or an artist, marking a track lights up both the title square and any of that artist's squares), reworked with a real host/guest split, live multiplayer sessions, and cover-art squares.
See PROPOSAL.md for the original design writeup.
Three ways to play
- Host a live session — connect your own Spotify (once, server-side), pick a playlist, get a join code + QR. A caller screen shows what's playing; guests' cards sync automatically, no login required on their end at all.
- Join a session — enter a host's code, get a card, watch it sync.
- Play solo — log into your own Spotify (PKCE, fully client-side, no server involvement) or paste any public playlist URL, get a one-off card with no session.
Real bugs found and fixed while building this
Worth documenting since they weren't obvious and would trip up anyone extending this:
- Spotify quietly renamed
/playlists/{id}/tracksto/playlists/{id}/items, with a reshaped response (entry.iteminstead ofentry.track). The old path returns a bare 403 with no useful error message — looks exactly like a permissions problem, isn't one. Confirmed directly against the live API, not assumed from docs. - Reading playlist tracks needs real user authentication, even for
fully public playlists — a pure Client Credentials (app-only) token
gets
401 "Valid user authentication required"on the/itemsendpoint. The public-playlist-by-URL feature routes through the host's already-authenticated connection instead of a dead-end app token — guests still never log into anything, it's just genuinely user-authenticated on the host's behalf. - Algorithmic/Spotify-owned editorial playlists (Discover Weekly, your own "Top 100" style auto-generated lists, etc.) are restricted from third-party API access — this is real and documented (Spotify's Nov 2024 API policy change), not a bug. They just won't appear as usable pools.
- Never point the client at an absolute cross-origin API URL — it
gets silently CORS-blocked with no server-side CORS headers configured,
and default error handling can mask that as "not connected" instead of
a clear network error. Use relative
/api/*paths everywhere; Vite's dev proxy and nginx's prod proxy both handle the same-origin routing. - Vite's dev server can bind to IPv6 loopback (
[::1]) only, depending on the host's DNS resolver order — silently unreachable via127.0.0.1, which matters a lot here since Spotify's redirect-URI security policy only exempts the literal127.0.0.1IP, notlocalhost.vite.config.tssetsserver.hostexplicitly.
Local development
# server
cd server && npm install && npm run dev # :3010
# client (separate terminal)
cd client && npm install && npm run dev # :3080 in prod, :5190 locally
Register four redirect URIs in the Spotify dashboard — one pair per
environment, host mode and guest mode each need their own. Exact string
match, trailing slash included: guest mode is ${window.location.origin}/
(client/src/lib/pkce.ts) — that trailing slash is not optional, ever,
in any environment.
| Environment | Host mode (/api/spotify/callback) |
Guest PKCE mode (root, no path) |
|---|---|---|
| Local dev | http://127.0.0.1:3010/api/spotify/callback |
http://127.0.0.1:5190/ |
| Production | https://<your-domain>/api/spotify/callback |
https://<your-domain>/ |
(127.0.0.1, not localhost, for local dev — Spotify's
HTTPS-required-for-redirect policy only exempts the literal loopback IP.)
All four need to actually exist in the dashboard's list — host mode and guest mode failing to match are indistinguishable from the outside ("redirect_uri: Not matching configuration" either way), so if this error shows up, check the browser's actual address bar at the point of failure to see which flow's redirect_uri is actually being sent before assuming which one is missing.
Deploying
docker compose up using docker-compose.yml / .env (copy
.env.example). Two images: web (nginx serving the built client,
proxying /api/* same-origin to the api container) and api (the
Express server). CI builds and pushes both on push to main.
VITE_SPOTIFY_CLIENT_ID gets baked into the client at build time
(Vite env vars are compile-time, not runtime) — passed as a Docker build
arg in CI, sourced from a SPOTIFY_CLIENT_ID repo secret.
Status
Verified end-to-end against a real Spotify account, not just built and
assumed working: host OAuth, real playlist listing and track reading
(after finding and fixing the /items rename), a live two-browser
session with the guest's card syncing on poll, self-mark toggling,
guest PKCE login and playlist reading, the public-playlist-by-URL path,
and auto-call correctly detecting a real track playing through Spotify
Connect within one poll tick.
License
MIT. Retains attribution to f8al's original bingo-bango design.