iPod.js: an iPod Classic in the browser, wired to Spotify and Apple Music
An iPod Classic simulator that connects to Apple Music and Spotify. Built with React & Styled Components
At a glance
- What is it?
- iPod Classic JS rebuilds the click wheel interface as a web app on Next.js and React, connects it to Spotify and Apple Music, and ships as a Next.js project deployed to Cloudflare Workers through OpenNext. The interesting engineering is in the plumbing: a service worker, a Cloudflare secrets store, and a redirect URI that has to be 127.0.0.1 rather than localhost.
- Who is it for?
- This project is worth reading if you want to build a music player with a real interface rather than a list of tracks, because the click wheel, the games and the themes are implemented in React and TypeScript and the player talks to two real streaming services.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 8 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A browser app that presents a physical interface
The project describes itself as the iPod Classic, rebuilt for the streaming era, and as Spotify and Apple Music in a pixel-perfect iPod Classic, complete with click wheel, games, and themes. A live instance is hosted at tannerv.com/ipod, and the author is Tanner Villarete.
What that means in practice is a web application that renders a device rather than a list of songs. The click wheel, the games and the themes are the visible layer, and the music comes from a streaming service. The description of the repository gives the same picture: an iPod Classic simulator that connects to Apple Music and Spotify, built with React and Styled Components. The primary language is TypeScript, the license is MIT, and the last push was on 2026-10-15.
There is no GitHub release history for the project, so there is no version stream to follow. The package manifest is marked private and carries version 0.1.0, which tells you how the author treats it: a published personal project rather than a packaged library, and the manifest version is not the version users install.
The value of the project is in the details rather than the concept. A click wheel is an unusual input device to reproduce in a browser, and doing it well means handling rotation, momentum and the physical feel of a press. The games and the themes are the other half, and they are the part that makes the result feel like an object rather than a skin. The dependency list backs up that reading: ios-haptics is present at version 0.1.4, which is a bridge for the vibration API on iOS, and motion at 12.38.0 handles the animation.
The quick start is two commands and a secrets prompt
Getting it running is short, and the README is precise about what happens on the first run.
pnpm install
pnpm devOn the first run you are prompted to enter values for each secret, which the README names as the Spotify credentials and the Apple Developer Token. Those values are stored in a local Cloudflare Secrets Store managed by Wrangler, which is an unusual choice for a local development environment and is the reason the deployment story later looks the way it does.
The app is then reachable at http://127.0.0.1:3000/ipod, and the README insists on the exact host, with a note that Spotify's redirect URIs require 127.0.0.1 instead of localhost. The same requirement is repeated in the secrets section, where you are told to add http://127.0.0.1:3000/ipod as a redirect URI in the Spotify app settings, and that it must be 127.0.0.1 and not localhost.
That instruction is worth taking literally. The Spotify OAuth flow matches the redirect URI against what is registered, and a mismatch between the registered value and what the browser sends is the kind of error that produces an unhelpful message from the identity provider rather than from the app. The port also matters, since the registered URI includes the port.
The package manager is pinned in the manifest at [email protected], and the lockfile is pnpm-lock.yaml, so pnpm is not optional here even though the commands look like npm ones.
Three secrets, and a dev command that refuses to start without them
The secrets section lists exactly three values, and the table pairs each with where to get it.
pnpm sync-dev-varsThat command runs the setup manually, and running pnpm dev will also detect missing secrets and prompt for values, so the setup path is available either way. The same command is the documented way to update local secret values at any time.
The three secrets are SPOTIFY_CLIENT_ID, created by making an app in the Spotify Developer Dashboard, SPOTIFY_CLIENT_SECRET, copied from the same Spotify app, and APPLE_DEVELOPER_TOKEN, for which the README points at a separate Apple Music JWT Generator repository by the same author. That is the entire authentication surface, and it is worth noting what is not in it. There is no session cookie configuration, no database URL and no API key for a backend, because there is no backend of your own to connect to. The Spotify credentials are the application's credentials, and the Apple token is a developer token for the MusicKit API.
The manifest shows how the check is wired in. The dev script is `node scripts/ensure-dev-secrets.js && NODE_OPTIONS='--inspect' next dev`, so the secret check runs as a separate script before the dev server starts, and the Node inspector is switched on by default. The second script in the manifest is `node scripts/sync-dev-vars.js`, which is the command behind pnpm sync-dev-vars. Both live in the scripts/ directory at the repository root, alongside a scripts/ensure-dev-secrets.js file, and there is no separate configuration file for them in the root listing.
For a fork, this is the part to read first, because it is where the environment assumptions live. A script that guarantees the secrets exist before the server boots is a good pattern, and it is also the reason the local setup feels automatic on a first run.
The deployment target is Cloudflare Workers, not a Node server
The production target is Cloudflare Workers, reached through OpenNext, and the build is a two step process rather than a single command.
pnpm build # Next.js + Serwist service worker
npx opennextjs-cloudflare build # Bundle for Cloudflare Workers
npx opennextjs-cloudflare deploy # Deploy to productionThe middle step is OpenNext, which is the adapter that turns a Next.js application into a Worker bundle. The manifest confirms the arrangement with a cf-deploy script that chains all three: `pnpm build && opennextjs-cloudflare build && opennextjs-cloudflare deploy`, and a preview script that builds and then runs `opennextjs-cloudflare preview` instead of deploying. The repository root carries open-next.config.ts for the adapter and wrangler.jsonc for the Worker configuration.
The service worker is part of the build rather than an afterthought. The build script is `next build && serwist build`, and the root has a serwist.config.js, with the Serwist CLI and the Next.js integration in the dev dependencies. The comment in the README names both pieces, Next.js plus a Serwist service worker, which is the standard way to make a web app installable and usable offline. There is also a generate-pwa-assets style script in the manifest, and the wrangler types generator, `wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts`, which keeps the type definitions for the Worker environment in sync with the configuration.
Production secrets are handled the same way as local ones, through the Cloudflare Secrets Store, managed either from the dashboard or with the wrangler secrets-store CLI. The result is a deployment with no long-lived server to operate, which fits a project of this size.
What the dependency list says about how the music is fetched
The manifest is short and reads like an inventory of the client's needs, which makes it a good way to understand the architecture without opening a component file.
Next.js 16.3.0 with React 19.2.5 and react-dom is the base, and styled-components 6.4.1 provides the styling, which is what makes a pixel-faithful device shell possible without a CSS framework getting in the way. @tanstack/react-query at 5.100.4 handles server state, and the manifest includes type packages for musickit-js, spotify-api and spotify-web-playback-sdk, which tells you the client talks to the Spotify Web API and the Web Playback SDK, and to Apple's MusicKit.
The presence of spotify-web-playback-sdk is the interesting one, since it means the browser is the playback client rather than an element pointed at an audio file, and the type packages for spotify-api and musickit-js sit alongside it for the two APIs the project talks to. The README does not describe the audio path in detail, so how playback is routed to the device is a question for the source rather than for the documentation.
Smaller pieces fill in the details. use-debounce at 10.1.1 is the kind of dependency a rotating input needs, uuid 14.0.0 and query-string 9.3.1 handle identifiers and URL state, and he 1.2.0 is an HTML entity encoder, which matters when track metadata from a third-party API is rendered as text.
The tooling is more thorough than the README suggests. Alongside eslint 9 with the Next.js config and a long list of plugins, there is @next/bundle-analyzer, a bundle-report script that runs the build with ANALYZE=true, esbuild 0.28.0, and a type-check script that calls tsgo rather than tsc, with a separate tsconfig.worker.json alongside the main tsconfig for the Worker side of the build.
Where iPod Classic JS is the wrong project to copy
The boundaries here are worth stating plainly, because the project is more capable than its size suggests and it is easy to assume things it does not do.
It is a web app, not a native one. There is no React Native, no Electron and no native module in the manifest. The installable experience comes from the Serwist service worker, which makes it a PWA, and the README's mention of the client app refers to that. A team that needs an app in the App Store, or a background playback mode on a locked phone screen, is looking at a different architecture.
It needs credentials you may not be able to get. The project expects a Spotify developer app and an Apple developer token, and it expects the person running it to create both accounts. That is a real barrier for a reader who just wants to look at the interface, and it is also why the project is not a neutral demo.
The visual fidelity claim is the author's own. The description says pixel-perfect, and there is no measurement, no test and no comparison procedure in the repository to back it up, so treat it as a design goal rather than a verified property. A forked version with a different font, a different artwork size or a different menu timing will drift from the original, and the fidelity is a property of the project as committed rather than of the code.
The scope is also fixed to two services. Spotify and Apple Music are the only two integrations, and there is no abstraction layer described for adding a third, no offline cache of audio beyond what the service worker provides, and no local library support. The my-list style features people expect from a personal music player, syncing favourites between the services, or gapless playback across sources, are not in the dependency list and are not mentioned anywhere in the documentation.
The repository is a single author's project with no release history, so if you depend on it, read the commits rather than a changelog, and note that the last push was on 2026-10-15, which means the project is being worked on but nothing is versioned for consumers.
Editorial conclusion
This project is worth reading if you want to build a music player with a real interface rather than a list of tracks, because the click wheel, the games and the themes are implemented in React and TypeScript and the player talks to two real streaming services. It is the wrong project to copy wholesale if you need a phone app, since the whole thing is a web app with a service worker rather than a native build, and it is the wrong project for anyone who does not want to hold Spotify and Apple Music credentials. Before running it, register the redirect URI as http://127.0.0.1:3000/ipod, and read the scripts in the scripts/ directory, since the dev command starts by ensuring secrets exist.
Frequently asked questions
How do I run iPod Classic JS locally?
Run pnpm install and then pnpm dev, then open http://127.0.0.1:3000/ipod. The dev script runs a check that detects missing secrets and prompts for their values before the Next.js dev server starts, and those values are kept in a local Cloudflare Secrets Store managed by Wrangler.
Why does the setup insist on 127.0.0.1 instead of localhost?
Spotify's redirect URIs require the 127.0.0.1 form. You have to add http://127.0.0.1:3000/ipod as a redirect URI in the Spotify app settings, and the README states it must be 127.0.0.1 and not localhost.
Which secrets does the project need?
Three: SPOTIFY_CLIENT_ID and SPOTIFY_CLIENT_SECRET from an app in the Spotify Developer Dashboard, and APPLE_DEVELOPER_TOKEN, which the README points to a separate Apple Music JWT Generator repository for. The command pnpm sync-dev-vars runs the setup manually or updates values later.
How is the app deployed?
On Cloudflare Workers through OpenNext. The build is pnpm build for the Next.js app plus the Serwist service worker, then npx opennextjs-cloudflare build to bundle for Workers, then npx opennextjs-cloudflare deploy. Production secrets live in the Cloudflare Secrets Store.
Which technologies is iPod Classic JS built with?
Next.js, React, TypeScript, Styled Components and Motion, as the README lists them. The package manifest adds React Query for server state, the Spotify Web API and Web Playback SDK types, the musickit-js types for Apple Music, and Serwist for the service worker.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/tvillarete-ipod-classic-js)Was this useful?