Spotified is a private, non-commercial Next.js companion for a Spotify account. It presents Spotify-supplied affinity rankings, recently played tracks, saved tracks, playlists, permitted playlist contents, and manual catalog search, and it gives qualifying Premium users full-length in-browser playback through Spotify’s official Web Playback SDK.
It deliberately does not calculate listening time, play counts, top genres, discovery rates, taste profiles, or personalized recommendations from Spotify data. Spotify's current Developer Policy restricts derived listening analytics and user profiling, and several older recommendation/audio endpoints are unavailable to new applications.
- Next.js 16 App Router, React, and strict TypeScript
- Tailwind CSS and local shadcn/ui components
- Supabase PostgreSQL
- Drizzle ORM and generated SQL migrations
- Spotify Web API with the server-side Authorization Code flow
- Spotify Web Playback SDK as a Premium-only browser Connect device
- Vitest, ESLint, and Prettier
- Vercel-compatible Node.js route handlers
- Node.js 24 or newer
- A qualifying Spotify Premium account that owns the Development Mode app
- A qualifying Spotify Premium subscription for every account that uses full-length web playback
- A Spotify Developer application with Web API and Web Playback SDK selected
- A Supabase project
Spotify Development Mode currently supports at most five allowlisted users. Add each test account under the app's Users Management page before connecting it.
-
Create an application in the Spotify Developer Dashboard.
-
Select both Web API and Web Playback SDK for the application. If this app already existed, review its current product selection in Settings.
-
Add this exact local redirect URI:
http://127.0.0.1:3000/api/auth/spotify/callbackSpotify does not accept
localhostas a redirect URI. Use the explicit loopback IP. -
For production, add the exact stable HTTPS callback, for example:
https://your-domain.example/api/auth/spotify/callback -
Add approved beta users in Users Management.
-
Keep the client secret server-side. Never prefix it with
NEXT_PUBLIC_.
The app requests only:
playlist-read-collaborativeplaylist-read-privatestreaminguser-modify-playback-stateuser-read-emailuser-read-privateuser-library-readuser-read-recently-playeduser-top-read
Accounts connected before playback was added must use Reconnect Spotify once to approve the expanded grant. user-read-email is included because Spotify’s SDK setup documents it; Spotified does not read, store, display, or use the account email.
Use the Supabase Shared Pooler / transaction mode URL for DATABASE_URL because Vercel functions are short-lived. Runtime Postgres.js connections disable prepared statements, as required by transaction pooling.
Use the direct or session-mode connection as DATABASE_MIGRATION_URL when your network supports it. This URL is used only by Drizzle CLI commands.
-
Install dependencies:
npm install
-
Copy
.env.exampleto.env.localand fill in the values. -
Generate a 32-byte token-encryption key:
node -e "console.log(require('node:crypto').randomBytes(32).toString('base64url'))"
-
Apply the committed migration to Supabase:
npm run db:migrate
-
Start the app:
npm run dev
-
Open
http://127.0.0.1:3000.
| Variable | Purpose |
|---|---|
APP_BASE_URL |
Stable origin used for same-origin checks, without a trailing slash |
DATABASE_URL |
Supabase transaction-pooler runtime connection |
DATABASE_MIGRATION_URL |
Direct/session connection for Drizzle migrations |
SPOTIFY_CLIENT_ID |
Spotify application client ID |
SPOTIFY_CLIENT_SECRET |
Server-only Spotify client secret |
SPOTIFY_REDIRECT_URI |
Exact registered callback URI |
TOKEN_ENCRYPTION_KEY |
Base64url-encoded 32-byte AES-256 key |
NEXT_PUBLIC_PRIVACY_CONTACT_EMAIL |
Contact shown in privacy and terms pages |
Never commit .env.local, tokens, database passwords, or provider secrets.
- Opening an authenticated page loads the official SDK and registers
Spotified Web Playeras a Spotify Connect device. It does not start audio automatically. - Choosing a song first activates the browser audio element, then asks the server to start that URI on the SDK device.
- Songs opened from a Spotify playlist retain that playlist context. Other pages queue the playable songs visible in their Spotify-provided order.
- The persistent player supports previous, play/pause, next, seek, volume, and Media Session controls while honoring restrictions returned by Spotify.
- Direct podcast playback is not included; podcast rows continue to open Spotify.
- The SDK requires a qualifying Premium plan. iOS still requires direct interaction before audio and may keep volume under physical device control.
npm run dev
npm run typecheck
npm run lint
npm run test
npm run build
npm run format:check
npm run db:generate
npm run db:migrateGenerate a new migration only after intentionally changing src/db/schema.ts. Review generated SQL before applying it. Do not use destructive schema pushes against production.
- OAuth state is random, short-lived, HTTP-only, and compared in constant time.
- Refresh tokens and Spotify client credentials never reach browser JavaScript or client API responses.
- The authenticated, uncached SDK token endpoint returns only a current short-lived access token. The browser passes it directly to the SDK without persisting it in React state, storage, logs, rendered markup, or URLs.
- Stored access and refresh tokens are encrypted with AES-256-GCM before entering PostgreSQL.
- Browser sessions use random opaque values; only SHA-256 hashes are stored.
- Session and OAuth cookies are HTTP-only, SameSite=Lax, and Secure in production.
- POST routes verify the request origin.
- A Spotify 401 triggers one forced refresh.
invalid_grantdiscards unusable tokens and requires reconnection. - Spotify refresh tokens expire six months after the original authorization under the current platform rules.
- Disconnecting deletes the local user, account credentials, and every session through foreign-key cascades.
- Personalized pages use request-time data and do not use a shared public cache.
- Push the repository to GitHub.
- Import it into Vercel.
- Configure all environment variables separately for production.
- Register the production HTTPS redirect URI and confirm Web Playback SDK is selected before testing OAuth.
- Apply database migrations explicitly from a trusted environment.
- Review and replace the privacy contact value.
- Have the project owner review the privacy and end-user terms before allowing another user to connect.
- Reconnect every previously authorized account so its grant includes the playback scopes.
Vercel Hobby is for personal, non-commercial use. Supabase Free currently pauses inactive projects and does not provide the same backup guarantees as a paid plan. A publicly reachable Vercel URL does not bypass Spotify's five-user Development Mode allowlist.
- The user-facing product name is Spotified.
- Spotify artwork is shown without cropping, overlays, distortion, or animation.
- Spotify metadata links back to the relevant Spotify page and includes source attribution.
- Streaming is Premium-only, user-initiated, private, and non-commercial; audio is never altered, mixed, synchronized, broadcast, or downloaded.
- The player honors SDK restrictions and always shows Spotify-supplied artwork and metadata with a Spotify link while audio is playing.
- Playlist contents are available only for playlists the account owns or collaborates on; a followed playlist can appear in Collections while its items remain unavailable.
- Search is manual and limited to Spotify's current maximum of ten results per item type.
- No Spotify recommendations, related-artists API, audio features, audio analysis, preview-audio dependency, playlist writes, Last.fm integration, or YouTube Music migration is included.
Unit tests use mocked provider responses and require no secrets. A real end-to-end OAuth and playback smoke test requires a configured Supabase database, Spotify application credentials, Web Playback SDK product access, a Premium app owner, and an allowlisted account with a qualifying Premium plan.