Melodex is a full-stack music trivia game built with Next.js App Router, Supabase auth and leaderboards, Deezer-powered preview clips, and a 3D animated homepage. Players listen to short preview clips, choose the correct song title, and play across standard, health, timer, and ranked leaderboard modes.
- 3D animated homepage built with Three.js.
- Deezer artist search and preview-clip based trivia questions.
- Standard mode: get 10 correct answers at your own pace.
- Health mode: get 10 correct answers before losing 3 hearts.
- Timer mode: get 10 correct answers before the 3-minute clock runs out.
- Leaderboard mode: authenticated ranked health and timer runs using random popular artists.
- Supabase auth, public profiles, avatar uploads, and account deletion.
- Leaderboard best-score saving per user and mode.
- HMAC-signed answer tokens so the client cannot know the correct answer until it submits.
- Track filtering for preview availability, duplicate title variants, and remix/mix variants.
- Shared JSON API envelope and typed validation with Zod.
- Next.js 15 App Router
- React 19
- TypeScript
- Tailwind CSS 4
- Supabase Auth, Database, and Storage
- Deezer API
- Three.js
- Framer Motion
- Zod
- Vitest
- Vercel Analytics
src/app: App Router pages, layouts, middleware-backed auth flows, and API route handlers.src/app/ui: Homepage, play-mode UI, auth menu, leaderboard table, and shared game panels.src/app/ui/play-mode/modes: Standard, health, timer, and leaderboard mode configs.src/lib/game: Pure game logic for modes, scoring, shuffling, dedupe, remix filtering, question building, leaderboard pools, and answer tokens.src/lib/providers: Music provider abstraction plus Deezer, Apple Music, and Spotify adapters.src/lib/validation: Zod schemas for artist, track, question, and answer requests.src/lib/leaderboard: Leaderboard request and response schemas.src/lib/utils: Fetch wrapper, app errors, logging, and helpers.src/utils/supabase: Browser, server, middleware, and service-role Supabase clients.supabase/migrations: Database, storage bucket, RLS, function, and grant migrations.docs/ui-components: Notes for reusable UI components.
| Mode | Route | Auth | Rules |
|---|---|---|---|
| Standard | /play/standard |
No | Pick an artist and reach 10 correct answers. |
| Health | /play/health |
No | Pick an artist, start with 3 hearts, and reach 10 correct answers before hearts run out. |
| Timer | /play/timer |
No | Pick an artist and reach 10 correct answers before the 3-minute clock expires. The timer freezes when the target is reached. |
| Leaderboard | /play/leaderboard |
Yes | Ranked health and timer runs using random popular artists. Scores save only when logged in. |
All providers implement the MusicProvider interface and return normalized artists and tracks.
deezer: Fully working. Artist search uses Deezer search, and track lookup uses the public artist top-tracks feed.apple: Scaffolded. Reports not configured unlessAPPLE_MUSIC_DEVELOPER_TOKENis present, then reports not implemented.spotify: Scaffolded. Reports not configured unlessSPOTIFY_CLIENT_IDandSPOTIFY_CLIENT_SECRETare present, then reports not implemented.
Provider query parameters default to deezer.
Before a track can appear in a question, the app:
- Requires a non-empty preview URL.
- Deduplicates near-identical titles such as live, remastered, radio edit, acoustic, instrumental, feature, and version variants.
- Removes remix-style tracks using visible titles, Deezer full titles, and album titles. This catches examples like
Remix,Remixes,Club Mix,Extended Mix,Bootleg, andMashup.
Supabase is used for auth, public profiles, avatars, and leaderboard persistence.
public.profilesuser_id uuid primary keydisplay_name textavatar_url text- timestamps
public.leaderboard_scores- one best score per
user_idandmode - supported modes:
health,timer - stores score, rounds played, accuracy, display name, avatar URL, and timestamps
- one best score per
avatarsbucket- public read
- max file size: 2 MB
- allowed MIME types: JPG, PNG, WebP
- users can upload, update, and delete files in their own folder.
public.submit_leaderboard_score(...)- called from the server with
SUPABASE_SERVICE_ROLE_KEY - inserts a user's first score for a mode
- updates only when the submitted score is better than the stored score
- returns
is_new_best
- called from the server with
RLS is enabled for exposed public tables. Browser clients get limited access through policies; service-role server routes handle leaderboard reads and writes.
Important production env:
SUPABASE_SERVICE_ROLE_KEYmust exist on the server only.- Never expose the service role key as
NEXT_PUBLIC_*.
Copy .env.local.example to .env.local:
GAME_ANSWER_SECRET=change-me-in-production
UPSTREAM_TIMEOUT_MS=8000
NEXT_PUBLIC_SUPABASE_URL=
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=
SUPABASE_SERVICE_ROLE_KEY=
APPLE_MUSIC_DEVELOPER_TOKEN=
APPLE_MUSIC_STOREFRONT=us
SPOTIFY_CLIENT_ID=
SPOTIFY_CLIENT_SECRET=Notes:
GAME_ANSWER_SECRETshould always be set in production. Local development falls back to a development secret if it is missing.NEXT_PUBLIC_SUPABASE_URLandNEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEYare required for auth and browser Supabase calls.SUPABASE_SERVICE_ROLE_KEYis required for leaderboard API routes and account deletion.- Apple Music and Spotify are optional scaffolds and do not block Deezer gameplay.
Install dependencies:
npm.cmd installStart the Next.js dev server:
npm.cmd run devThe app runs on:
http://127.0.0.1:3030If the Next.js cache gets corrupted on Windows, clear it:
npm.cmd run clean
npm.cmd run devThis repo includes Supabase config and migrations. If you are working locally with Supabase CLI:
supabase start
supabase db reset
supabase status -o envUse the values from supabase status -o env to fill local Supabase env vars. For a hosted project, apply migrations with the Supabase CLI after linking the project:
supabase link --project-ref <project-ref>
supabase db pushnpm.cmd run dev # Start Next.js on port 3030
npm.cmd run build # Production build
npm.cmd run start # Start production server on port 3030
npm.cmd run typegen # Generate Next.js route types
npm.cmd run typecheck # Generate route types and run TypeScript
npm.cmd run test # Run Vitest
npm.cmd run clean # Remove .next and .next-dev/: 3D homepage and game-mode selector./play: redirects or resolves the default play flow./play/standard: standard artist-based trivia./play/health: hearts-based artist trivia./play/timer: timed artist trivia./play/leaderboard: authenticated ranked health and timer modes./leaderboard: public leaderboard table./login: sign in and sign up./settings: authenticated profile, avatar, and account settings./auth/callback: Supabase auth callback.
All API success responses use:
{
"data": {}
}Errors use:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "The request payload was invalid.",
"details": {}
}
}Returns service health, current timestamp, whether the fallback answer secret is active, and provider readiness.
curl.exe http://127.0.0.1:3030/api/healthSearches artists through the selected provider.
curl.exe "http://127.0.0.1:3030/api/artists/search?q=Taylor%20Swift&provider=deezer"Returns normalized tracks for an artist.
curl.exe "http://127.0.0.1:3030/api/artists/12246/tracks?provider=deezer"Builds a question from filtered previewable tracks and returns a signed answer token.
curl.exe "http://127.0.0.1:3030/api/game/question?artistId=12246&mode=standard&provider=deezer"Response shape:
{
"data": {
"questionId": "uuid",
"artistId": "12246",
"mode": "standard",
"provider": "deezer",
"previewUrl": "https://...",
"options": [
{
"trackId": "1",
"title": "Song A",
"artworkUrl": "https://..."
}
],
"answerToken": "signed-token",
"totalPlayableTrackCount": 24
}
}Validates a signed answer token and returns the result.
curl.exe -X POST "http://127.0.0.1:3030/api/game/answer" ^
-H "Content-Type: application/json" ^
-d "{\"mode\":\"standard\",\"questionId\":\"<uuid>\",\"selectedTrackId\":\"<track-id>\",\"answerToken\":\"<token>\"}"Authenticated endpoint for ranked runs. Chooses from a shuffled pool of popular artists and returns a signed question.
Returns ranked leaderboard scores for health or timer.
Authenticated endpoint that submits a completed ranked run.
{
"mode": "health",
"score": 12,
"roundsPlayed": 15,
"accuracy": 80
}Authenticated endpoint that signs the user out, removes their avatar, and deletes the Supabase auth user through the service-role client.
UNAUTHORIZEDVALIDATION_ERRORPROVIDER_NOT_CONFIGUREDPROVIDER_NOT_IMPLEMENTEDNOT_FOUNDINSUFFICIENT_TRACKSCATALOG_EXHAUSTEDUPSTREAM_PROVIDER_ERRORINVALID_ANSWER_TOKENINTERNAL_ERROR
Typical HTTP mapping:
400: validation, unsupported provider, or invalid answer token401: authenticated route without a valid session404: artist or tracks not found422: fewer than 4 usable tracks or exhausted catalog502: upstream provider error or timeout500: unexpected server error
Run:
npm.cmd run test
npm.cmd run typecheckCurrent coverage focuses on pure logic:
- answer token signing, verification, expiry, and tamper protection
- question building
- title dedupe and remix filtering
- shuffle behavior
- game mode resolution
- route validation schemas
For production:
- Set
GAME_ANSWER_SECRETto a strong secret. - Set all Supabase env vars in the hosting platform.
- Keep
SUPABASE_SERVICE_ROLE_KEYserver-only. - Apply all Supabase migrations to the production project.
- Confirm Supabase auth redirect URLs include the production domain and
/auth/callback. - Deezer works without provider credentials; Apple Music and Spotify are placeholders.
- Deezer is the only fully working music provider.
- Deezer track lookup uses the public artist top-tracks feed, not a full discography crawl.
- Leaderboard mode supports ranked health and timer runs only.
- Scores save one best score per user per ranked mode.
- Answer validation is stateless and based on signed tokens.