Skip to main content
RapidDev - Software Development Agency

How to Build Music streaming backend with V0

Build a music catalog and streaming backend with V0 using Next.js, Supabase Storage, and shadcn/ui. Features audio file management, streaming endpoints with range request support, playlist CRUD, play count analytics, and signed URLs for private audio files. Takes about 2-4 hours.

What you'll build

  • Audio streaming API with Supabase Storage signed URLs and range request support for seeking
  • Music catalog with artists, albums, and tracks displayed using shadcn/ui Card and Table components
  • Playlist CRUD with drag-and-drop track reordering and inline audio player
  • Play count tracking and listening history with analytics dashboard
  • Private audio bucket ensuring files are only accessible via time-limited signed URLs
  • Now-playing queue with Sheet slide-over showing upcoming tracks
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Advanced9 min read2-4 hoursV0 Premium or higherApril 2026RapidDev Engineering Team
TL;DR

Build a music catalog and streaming backend with V0 using Next.js, Supabase Storage, and shadcn/ui. Features audio file management, streaming endpoints with range request support, playlist CRUD, play count analytics, and signed URLs for private audio files. Takes about 2-4 hours.

What you're building

Independent music platforms and podcast hosts need a backend for managing audio catalogs, serving streams, and tracking listener analytics. Building this on top of Supabase Storage gives you private file hosting with signed URL access control.

V0 generates the catalog UI, playlist management, and player components from prompts. Supabase Storage handles audio file hosting in a private bucket, and the streaming API route generates signed URLs with CDN caching headers for performance.

The architecture uses a private Supabase Storage bucket for audio files, an API route that generates signed URLs for streaming, Server Components for the catalog and playlists, and a client component audio player with shadcn/ui Slider for scrubbing and volume control.

Final result

A music streaming backend with catalog management, playlist CRUD, audio streaming via signed URLs, play tracking, and an inline audio player.

Tech stack

V0AI Code Generator
Next.jsFull-Stack Framework
Tailwind CSSStyling
shadcn/uiComponent Library
SupabaseDatabase
Supabase StorageStorage

Prerequisites

  • A V0 account (Premium or higher — this is a complex build)
  • A Supabase project with Storage enabled (Pro recommended for storage limits)
  • Audio files to upload (MP3 or AAC format, under 100MB each)
  • Your music catalog structure (artists, albums, tracks)

Build steps

1

Set up the database schema and private audio bucket

Create the Supabase schema for artists, albums, tracks, playlists, and play history. Configure a private Storage bucket for audio files.

prompt.txt
1// Paste this prompt into V0's AI chat:
2// Build a music streaming backend. Create a Supabase schema:
3// 1. artists: id (uuid PK), user_id (uuid FK to auth.users), name (text), bio (text), image_url (text)
4// 2. albums: id (uuid PK), artist_id (uuid FK to artists), title (text), cover_url (text), release_date (date)
5// 3. tracks: id (uuid PK), album_id (uuid FK to albums), artist_id (uuid FK to artists), title (text), duration_seconds (int), file_url (text), file_size_bytes (bigint), genre (text), play_count (int DEFAULT 0)
6// 4. playlists: id (uuid PK), user_id (uuid FK to auth.users), name (text), description (text), is_public (boolean DEFAULT true), created_at (timestamptz)
7// 5. playlist_tracks: playlist_id (uuid FK to playlists), track_id (uuid FK to tracks), position (int), added_at (timestamptz), PRIMARY KEY (playlist_id, track_id)
8// 6. play_history: id (uuid PK), user_id (uuid FK to auth.users), track_id (uuid FK to tracks), played_at (timestamptz), duration_listened (int)
9// Add RLS policies. Also create a private Storage bucket called 'audio' with 100MB file size limit.

Pro tip: Use a private bucket for audio files. This means files cannot be accessed directly — you control access through signed URLs generated server-side.

Expected result: All tables are created with RLS policies. A private 'audio' Storage bucket is configured with 100MB file limit.

2

Build the audio streaming API with signed URLs

Create the API route that generates signed URLs for audio streaming. The signed URL has a 1-hour expiry and is served with CDN caching headers for performance.

app/api/stream/[trackId]/route.ts
1import { createClient } from '@supabase/supabase-js'
2import { NextRequest, NextResponse } from 'next/server'
3
4const supabase = createClient(
5 process.env.SUPABASE_URL!,
6 process.env.SUPABASE_SERVICE_ROLE_KEY!
7)
8
9export async function GET(
10 req: NextRequest,
11 { params }: { params: Promise<{ trackId: string }> }
12) {
13 const { trackId } = await params
14
15 const { data: track } = await supabase
16 .from('tracks')
17 .select('file_url, title')
18 .eq('id', trackId)
19 .single()
20
21 if (!track?.file_url) {
22 return NextResponse.json({ error: 'Track not found' }, { status: 404 })
23 }
24
25 const { data: signedUrl } = await supabase.storage
26 .from('audio')
27 .createSignedUrl(track.file_url, 3600)
28
29 if (!signedUrl) {
30 return NextResponse.json({ error: 'Failed to generate URL' }, { status: 500 })
31 }
32
33 return NextResponse.redirect(signedUrl.signedUrl, {
34 headers: {
35 'Cache-Control': 'public, max-age=3600',
36 'Accept-Ranges': 'bytes',
37 },
38 })
39}

Expected result: The streaming endpoint generates a signed URL and redirects the audio player to it. URLs expire after 1 hour.

3

Build the music catalog and playlist UI

Create the catalog browsing pages and playlist management interface with an inline audio player component.

prompt.txt
1// Paste this prompt into V0's AI chat:
2// Create music streaming pages:
3// 1. app/page.tsx — featured playlists Card grid + trending tracks Table (title, artist, album, duration, play_count, play Button)
4// 2. app/library/page.tsx — user's playlists grid + recently played history Table
5// 3. app/playlist/[id]/page.tsx — tracklist Table with position, title, artist, duration. Add inline play Button per track. Show playlist cover, name, description, track count.
6// 4. Add a persistent audio player bar at the bottom: track title + artist, Slider for scrubber, volume Slider, play/pause/skip Buttons, Sheet for now-playing queue
7// Use shadcn/ui Card for albums, Table for tracklists, Slider for audio controls, DropdownMenu for track options (add to playlist, share), Sheet for queue.

Pro tip: Store SUPABASE_SERVICE_ROLE_KEY in V0's Vars tab without NEXT_PUBLIC_ prefix — it is needed server-side to generate signed Storage URLs for private audio files.

Expected result: The catalog shows featured playlists and trending tracks. Playlists display tracklists with play buttons. A persistent player bar controls audio.

4

Implement play count tracking and analytics

Create the API route that increments play counts and logs listening history when a track is played. This data powers trending tracks and user history features.

app/api/tracks/play/route.ts
1import { createClient } from '@supabase/supabase-js'
2import { NextRequest, NextResponse } from 'next/server'
3
4const supabase = createClient(
5 process.env.SUPABASE_URL!,
6 process.env.SUPABASE_SERVICE_ROLE_KEY!
7)
8
9export async function POST(req: NextRequest) {
10 const { track_id, user_id, duration_listened } = await req.json()
11
12 if (!track_id) {
13 return NextResponse.json({ error: 'Missing track_id' }, { status: 400 })
14 }
15
16 await supabase.rpc('increment_play_count', { p_track_id: track_id })
17
18 if (user_id) {
19 await supabase.from('play_history').insert({
20 user_id,
21 track_id,
22 played_at: new Date().toISOString(),
23 duration_listened: duration_listened || 0,
24 })
25 }
26
27 return NextResponse.json({ success: true })
28}

Expected result: Play counts increment atomically. User listening history is logged for recently played and analytics features.

5

Add playlist CRUD and deploy

Build playlist creation, track adding/removing, and reordering. Then deploy the streaming backend.

prompt.txt
1// Paste this prompt into V0's AI chat:
2// Add playlist management:
3// 1. Server Action createPlaylist: insert playlist with name, description, is_public
4// 2. Server Action addTrackToPlaylist: insert into playlist_tracks with max position + 1
5// 3. Server Action removeTrackFromPlaylist: delete from playlist_tracks and reorder positions
6// 4. Server Action reorderTrack: update position values for drag-and-drop reordering
7// 5. 'New Playlist' Dialog on library page: name Input, description Textarea, public Switch
8// 6. On track DropdownMenu, add 'Add to Playlist' option that opens a Dialog listing user's playlists with add Buttons
9// 7. On playlist page, add DropdownMenu per track: Remove from playlist, Move up, Move down
10// Also create the Supabase RPC function: increment_play_count that does UPDATE tracks SET play_count = play_count + 1 WHERE id = p_track_id

Expected result: Playlists support creation, track adding/removing, and reordering. Play counts increment reliably. The app is deployed to Vercel.

Complete code

app/api/stream/[trackId]/route.ts
1import { createClient } from '@supabase/supabase-js'
2import { NextRequest, NextResponse } from 'next/server'
3
4const supabase = createClient(
5 process.env.SUPABASE_URL!,
6 process.env.SUPABASE_SERVICE_ROLE_KEY!
7)
8
9export async function GET(
10 req: NextRequest,
11 { params }: { params: Promise<{ trackId: string }> }
12) {
13 const { trackId } = await params
14
15 const { data: track } = await supabase
16 .from('tracks')
17 .select('file_url, title, duration_seconds')
18 .eq('id', trackId)
19 .single()
20
21 if (!track?.file_url) {
22 return NextResponse.json({ error: 'Track not found' }, { status: 404 })
23 }
24
25 const { data } = await supabase.storage
26 .from('audio')
27 .createSignedUrl(track.file_url, 3600)
28
29 if (!data?.signedUrl) {
30 return NextResponse.json(
31 { error: 'URL generation failed' },
32 { status: 500 }
33 )
34 }
35
36 return NextResponse.json({
37 stream_url: data.signedUrl,
38 title: track.title,
39 duration: track.duration_seconds,
40 })
41}

Customization ideas

Personalized recommendations

Build a simple recommendation engine that suggests tracks based on listening history genre preferences using a collaborative filtering query.

Waveform visualization

Add an audio waveform display using the wavesurfer.js library, showing the audio shape and current playback position.

Artist analytics dashboard

Build a dashboard for artists showing play counts over time, top listeners, geographic distribution, and revenue from streams.

Offline playback caching

Implement a service worker that caches recently played tracks for offline listening using the Cache API.

Common pitfalls

Pitfall: Using a public Storage bucket for audio files

How to avoid: Use a private bucket and generate signed URLs with 1-hour expiry through the streaming API. This forces all access through your application.

Pitfall: Exposing SUPABASE_SERVICE_ROLE_KEY with NEXT_PUBLIC_ prefix

How to avoid: Store it in V0's Vars tab without any prefix. Only use it in API routes that generate signed URLs.

Pitfall: Incrementing play_count with a regular UPDATE instead of atomic operation

How to avoid: Use a Supabase RPC function: UPDATE tracks SET play_count = play_count + 1 WHERE id = p_track_id. The database handles concurrency.

Best practices

  • Use a private Supabase Storage bucket for audio files and generate signed URLs server-side with 1-hour expiry
  • Store SUPABASE_SERVICE_ROLE_KEY in V0's Vars tab without NEXT_PUBLIC_ prefix for server-only signed URL generation
  • Use Supabase RPC for atomic play count increments to handle concurrent plays correctly
  • Add CDN caching headers (Cache-Control: public, max-age=3600) to streaming responses for performance
  • Set a 100MB file size limit on the audio bucket to prevent abuse
  • Use V0's Design Mode (Option+D) to adjust the player bar height, album card sizes, and tracklist spacing without spending credits
  • Implement the audio player as a persistent client component in the layout, not per-page, to prevent interrupting playback on navigation

AI prompts to try

Copy these prompts to build this project faster.

ChatGPT Prompt

I'm building a music streaming backend with Next.js App Router and Supabase Storage. I need an API route at app/api/stream/[trackId]/route.ts that looks up the track's file_url in the database, generates a signed URL from the private 'audio' Storage bucket with 1-hour expiry, and returns it. The audio player will use this URL for playback. Please also write the PostgreSQL function increment_play_count that atomically increments the play_count column.

Build Prompt

Create a persistent audio player component that sits in the app layout and survives page navigation. It should accept a track queue, display current track title and artist, have a Slider for scrubbing (using currentTime and duration), a volume Slider, play/pause/skip Buttons, and a Sheet showing the upcoming queue. Use the HTML5 Audio element wrapped in a React ref. Fetch the signed stream URL from /api/stream/[trackId] when a new track starts.

Frequently asked questions

Why use a private bucket instead of public for audio files?

A public bucket allows anyone to download files directly with the URL. With a private bucket, all access goes through your API which generates time-limited signed URLs. This prevents hotlinking and lets you track plays accurately.

Does the audio player support seeking (skipping to a specific time)?

Yes. The signed URL points to the actual audio file which supports HTTP range requests natively. The HTML5 Audio element handles seeking automatically when users drag the Slider scrubber.

Do I need a paid Supabase plan?

The free tier (1GB storage) works for small catalogs. For production with many audio files, the Pro plan ($25/month, 100GB storage) is recommended. Audio files are typically 3-10MB each.

Do I need a paid V0 plan?

Yes, Premium ($20/month) at minimum. The streaming backend has multiple complex pages (catalog, playlists, player, analytics) and API routes that require many prompts.

How do I upload audio files?

Upload through the Supabase Dashboard Storage interface for initial setup. For production, build an artist upload page that uses the Supabase Storage SDK to upload files to the private 'audio' bucket and stores metadata in the tracks table.

How do I deploy the streaming backend?

Click Share in V0, then Publish to Production. Set SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY in the Vars tab. Create the private 'audio' bucket in Supabase Dashboard before deploying.

Can RapidDev help build a custom music streaming platform?

Yes. RapidDev has built over 600 apps including media streaming platforms with CDN integration, analytics dashboards, and recommendation engines. Book a free consultation to discuss your platform requirements.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help building your app?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.