Was a Vite build-time env var, requiring a second copy of a non-secret value as a separate Gitea CI secret alongside the one already in .env for the api service. Client id isn't sensitive (public in every Spotify authorize URL), so it doesn't need build-time baking at all — now fetched once from a new GET /api/config on the already-running api server, cached client-side after the first call. Collapses config back down to the one place it already lived (the deploy host's .env / docker-compose environment), removes the CI build-arg and its secret entirely. Verified end-to-end: /api/config returns the real client id, and a live guest-login click correctly carries it through into the actual Spotify authorize URL (decoded and checked, not just assumed from the code).
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 two redirect URIs in the Spotify dashboard (exact string match, trailing slash matters):
http://127.0.0.1:3010/api/spotify/callback— host modehttp://127.0.0.1:5190/— guest PKCE mode (root, no path)
(127.0.0.1, not localhost — Spotify's HTTPS-required-for-redirect
policy only exempts the literal loopback IP.)
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.