remix this app

build your own mixtape site.

This whole app was built with Claude Code. Copy the prompt below and paste it into Claude Code to generate your own version — with Apple Music search, a custom cassette player, and shareable tape links.

claude code prompt
Build a web app called "Mixtape" that lets users create and share music playlists styled like 90s cassette tapes. Use Next.js (App Router) deployed on Cloudflare Workers.

## What to build

### 1. Create flow (3 steps)
- Step 1 /create — Search Apple Music, add songs to Side A and Side B (max 12 tracks per side). Give the tape a name and subtitle.
- Step 2 /design — Pick a cassette shell colour theme and j-card pattern. Add a recipient name ("who's it for?") and optional liner notes.
- Step 3 /preview — Preview the cassette and j-card. Save the tape and get a shareable URL.

### 2. Player page /t/[id]
- Arrival screen: shows the cassette floating, recipient greeting, and a "tap to insert tape" button.
- Player screen: animated cassette with spinning reels, transport controls (play/pause/rewind/fast-forward), track list, and liner notes tab.
- Uses Apple Music for playback — 30-second previews for non-subscribers, full tracks for Apple Music subscribers.

## Tech stack
- Next.js 15 with App Router, all pages as client components
- Cloudflare Workers via @opennextjs/cloudflare
- Cloudflare KV for tape persistence
- MusicKit JS v3 (loaded globally via <script> tag in layout) for Apple Music search and playback
- Apple Developer JWT (ES256) for MusicKit authentication

## Apple Music setup

### 1. Apple Developer account
Sign up at developer.apple.com ($99/year required for MusicKit).

### 2. Create a MusicKit key
- Go to Certificates, Identifiers & Profiles → Keys
- Create a new key, enable MusicKit
- Download the .p8 private key file — you only get one chance to download it
- Note your Key ID and your Team ID (shown top-right in the portal)

### 3. Environment variables
Set these in your deployment environment (and .env.local for local dev):

  APPLE_TEAM_ID=XXXXXXXXXX        # 10-character team ID
  APPLE_KEY_ID=XXXXXXXXXX         # key ID from the portal
  APPLE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----
  ...contents of your .p8 file...
  -----END PRIVATE KEY-----"

### 4. Token API route /api/token
Generate a MusicKit JWT on the server. Use the Web Crypto API directly (not jose) to avoid Node.js compatibility issues on Cloudflare Workers:

  const pem = privateKeyPem.replace(/\\n/g, '\n');
  const b64 = pem
    .replace(/-----BEGIN PRIVATE KEY-----/, '')
    .replace(/-----END PRIVATE KEY-----/, '')
    .replace(/\s+/g, '');
  const der = Uint8Array.from(atob(b64), c => c.charCodeAt(0));
  const key = await crypto.subtle.importKey(
    'pkcs8', der.buffer,
    { name: 'ECDSA', namedCurve: 'P-256' },
    false, ['sign']
  );
  const header = base64url(JSON.stringify({ alg: 'ES256', kid: keyId }));
  const payload = base64url(JSON.stringify({
    iss: teamId,
    iat: Math.floor(Date.now() / 1000),
    exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 180,
  }));
  const sig = await crypto.subtle.sign(
    { name: 'ECDSA', hash: { name: 'SHA-256' } },
    key,
    new TextEncoder().encode(`${header}.${payload}`)
  );
  return `${header}.${payload}.${base64url(sig)}`;

### 5. MusicKit initialisation
Load MusicKit globally in layout.js:
  <script src="https://js-cdn.music.apple.com/musickit/v3/musickit.js" data-web-components async />

Initialise with:
  await MusicKit.configure({ developerToken: token, app: { name: 'Mixtape', build: '1.0' } });

Search for songs using a template literal for the storefront (not ':storefront'):
  const result = await music.api.music(`/v1/catalog/${storefront}/search`, {
    term: query, types: 'songs', limit: 20,
  });

## Cloudflare setup

### KV namespace
  wrangler kv namespace create TAPES_KV

Add to wrangler.jsonc:
  "kv_namespaces": [{ "binding": "TAPES_KV", "id": "YOUR_ID" }]
  "compatibility_flags": ["nodejs_compat"]

Access KV in API routes:
  import { getCloudflareContext } from '@opennextjs/cloudflare';
  const ctx = await getCloudflareContext({ async: true });
  const kv = ctx.env.TAPES_KV;
  await kv.put(id, JSON.stringify(tape), { expirationTtl: 60 * 60 * 24 * 365 * 2 });

### Build config (open-next.config.ts)
  import { defineCloudflareConfig } from '@opennextjs/cloudflare';
  export default defineCloudflareConfig();

## Data model
Each tape stored in KV as JSON:
  {
    id: string,           // 8-char alphanumeric
    name: string,         // tape name
    subtitle: string,
    recipientName: string,
    fromName: string,
    linerNotes: string,
    colorTheme: string,   // 'classic' | 'ocean' | 'sunset' | 'forest' | 'purple-rain' | 'peach'
    pattern: string,      // 'none' | 'dots' | 'lines' | 'crosses' | 'zigzag' | 'stars'
    visibility: string,   // 'public' | 'private'
    sideA: Track[],       // max 12
    sideB: Track[],       // max 12
    editToken: string,    // 24-char secret for editing
    createdAt: string,
  }

  Track: { appleMusicId, title, artist, album, duration, artworkUrl }

Use localStorage to persist draft state across the create → design → preview flow.

## Cassette component
Build a custom animated SVG cassette (viewBox 0 0 420 262) with:
- Cassette body, label area, window cutout, reels, hub, spokes, accent stripe
- Reel animation using requestAnimationFrame and setAttribute('transform', rotate(...))
  rather than CSS (more reliable cross-browser)
- 2°/frame during normal play, 15°/frame during fast-forward

## Aesthetic
- Kraft paper texture background for the maker flow (CSS SVG background-image crosshatch)
- Dark background (#0d0d0d) for the player
- Fonts via Google Fonts link tag (not next/font/google — no build-time network on Cloudflare):
  Permanent Marker, Special Elite, Caveat, Patrick Hand
- Sticker-style buttons with box-shadow offsets and slight CSS rotations
- Cards with 2-3px border and 4px box-shadow for a lo-fi zine feel

## Important gotchas
- Use the Web Crypto API for JWT signing, NOT jose (jose uses Node.js KeyObject internally which breaks on Cloudflare Workers even with nodejs_compat)
- MusicKit search URL must use a template literal: /v1/catalog/${storefront}/search — not the literal string ':storefront'
- NEXT_PUBLIC_ env vars are baked in at build time; if using Cloudflare dashboard vars, fetch them from a server-side /api/config route instead
- KV operations are async — always await saveTape() and getTape() calls

Build the full working app end to end.

You'll need an Apple Developer account ($99/year) to get a MusicKit key. The prompt includes full setup instructions. Deploys best on Cloudflare Workers — free tier covers a lot of traffic.

Want to learn how to build apps like this? Sign up for Vibe Shift ↗