A modern, scalable music discovery platform built with Next.js 15 and Supabase, designed for African artists. The application features comprehensive content management, secure file uploads, and a responsive user interface with dark mode support.
- Features
- Architecture
- Technology Stack
- Prerequisites
- Installation & Setup
- Database Schema
- API Documentation
- Authentication & Security
- File Upload System
- SEO & Metadata
- Development
- Deployment
- Troubleshooting
- Contributing
- License
- Artist Profiles: Complete artist management with biographies, images, and multi-genre support
- Song Library: Audio file uploads with metadata extraction, album art, and featured artist support
- Albums: Album management with cover art and track listings
- Genres: Categorization system supporting multiple genres per item
- News System: Content publishing with markdown support and multiple image uploads
- Video Integration: YouTube video embedding linked to artists
- Responsive Design: Mobile-first approach with adaptive layouts
- Theme System: Light and dark mode with seamless transitions
- Search Functionality: Real-time search across artists, songs, and genres
- Audio Player: Built-in HTML5 audio player with streaming support
- Share Functionality: Social media sharing with Open Graph and Twitter Card metadata
- SEO Optimization: Dynamic meta tags, slug-based URLs, and sitemap generation
- Direct File Uploads: Browser-to-Supabase uploads bypassing server limitations
- Row Level Security: Granular database access control
- Server-Side Rendering: Optimized performance with Next.js 15 App Router
- Type Safety: Full TypeScript implementation
- Real-time Updates: Automatic content synchronization
- Error Handling: Comprehensive error logging and user feedback
The application follows Next.js 15 App Router conventions with a clear separation of concerns:
app/
├── (pages)/ # Public-facing pages
│ ├── artists/[id]/ # Dynamic artist detail pages
│ ├── songs/[id]/ # Dynamic song detail pages
│ ├── news/[id]/ # Dynamic news detail pages
│ ├── genres/[id]/ # Dynamic genre detail pages
│ └── videos/[id]/ # Dynamic video detail pages
├── admin/ # Protected admin dashboard
│ └── dashboard/ # Admin content management
├── api/ # API route handlers
│ ├── songs/ # Song CRUD operations
│ ├── artists/ # Artist CRUD operations
│ ├── albums/ # Album CRUD operations
│ ├── genres/ # Genre CRUD operations
│ ├── news/ # News CRUD operations
│ ├── videos/ # Video CRUD operations
│ ├── upload/ # File upload endpoint
│ ├── download/ # Secure file download proxy
│ └── admin/ # Admin authentication
└── globals.css # Global styles and theme variables
- Client Requests: User interactions trigger API calls or page navigations
- API Routes: Server-side handlers process requests with validation
- Supabase Client: Authenticated requests to Supabase backend
- Database Operations: PostgreSQL queries with RLS enforcement
- Storage Operations: Direct uploads to Supabase Storage buckets
- Response Handling: JSON responses with error handling
- UI Updates: React state management and re-rendering
- Framework: Next.js 15.2.4 (React 19)
- Language: TypeScript 5.x
- Styling: Tailwind CSS 4.x
- UI Components: Radix UI primitives
- Icons: Lucide React
- Theme: next-themes for dark/light mode switching
- Database: Supabase (PostgreSQL 15)
- Storage: Supabase Storage (S3-compatible)
- Authentication: Custom password-based admin auth
- API: Next.js API Routes (Edge Runtime compatible)
- Package Manager: pnpm
- Linting: ESLint with Next.js config
- Type Checking: TypeScript strict mode
- Version Control: Git
Before setting up the project, ensure you have:
- Node.js: Version 18.x or higher
- pnpm: Version 8.x or higher (
npm install -g pnpm) - Supabase Account: Free tier available at supabase.com
- Git: For version control
- Modern Browser: Chrome, Firefox, Safari, or Edge (latest versions)
git clone https://github.com/ochudi/biztune.git
cd biztune
pnpm installCreate a .env.local file in the project root:
cp .env.example .env.localConfigure the following environment variables:
# Supabase Configuration
NEXT_PUBLIC_SUPABASE_URL=https://your-project-id.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key-here
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key-here
# Admin Authentication
ADMIN_PASSWORD=your-secure-password-here
# Application URL (for production)
NEXT_PUBLIC_SITE_URL=https://your-domain.com
# Environment
NODE_ENV=developmentImportant: Never commit .env.local to version control. The .gitignore file is configured to exclude it.
- Go to supabase.com/dashboard
- Click "New Project"
- Choose organization and region
- Set a strong database password
- Wait for project initialization
Navigate to SQL Editor in Supabase dashboard and execute the following scripts in order:
Step 1: Create Tables
-- Execute scripts/create_supabase_tables.sql
-- This creates: artists, songs, albums, genres, news, videos tablesStep 2: Add Slug Support
-- Execute scripts/add_slug_columns.sql
-- Adds SEO-friendly URL slugs to artists, songs, news, videosStep 3: Add Song Descriptions
-- Execute scripts/add_song_description.sql
-- Adds description field to songs tableStep 4: Enable Row Level Security
-- Execute scripts/enable_rls_policies.sql
-- Configures RLS policies for secure data accessStep 5: Configure Storage
-- Execute scripts/configure_supabase_storage.sql
-- Sets up storage buckets and access policies-
Navigate to Storage in Supabase dashboard
-
Create two buckets:
-
Bucket Name:
audio- Public: Yes
- File size limit: 50MB
- Allowed MIME types:
audio/*
-
Bucket Name:
images- Public: Yes
- File size limit: 10MB
- Allowed MIME types:
image/*
-
-
Verify bucket settings:
SELECT * FROM storage.buckets;
pnpm devAccess the application:
- Frontend: http://localhost:3000
- Admin Panel: http://localhost:3000/admin
- Navigate to http://localhost:3000/admin
- Enter the password configured in
ADMIN_PASSWORD - Access the dashboard to begin content management
CREATE TABLE public.artists (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
bio TEXT,
image TEXT,
genre_ids TEXT[],
is_featured BOOLEAN DEFAULT false,
slug TEXT UNIQUE,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);CREATE TABLE public.songs (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
artist_id TEXT REFERENCES public.artists(id) ON DELETE SET NULL,
album_id TEXT REFERENCES public.albums(id) ON DELETE SET NULL,
genre_ids TEXT[],
featured_artist_ids TEXT[],
album_art TEXT,
download_url TEXT NOT NULL,
duration TEXT,
description TEXT,
slug TEXT UNIQUE,
created_at TIMESTAMPTZ DEFAULT NOW()
);CREATE TABLE public.albums (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
artist_id TEXT REFERENCES public.artists(id) ON DELETE SET NULL,
cover_art TEXT,
release_year INTEGER,
genre_ids TEXT[]
);CREATE TABLE public.genres (
id TEXT PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
description TEXT
);CREATE TABLE public.news (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
content TEXT,
image TEXT,
supporting_images TEXT[],
slug TEXT UNIQUE,
created_at TIMESTAMPTZ DEFAULT NOW()
);CREATE TABLE public.videos (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
youtube_url TEXT NOT NULL,
description TEXT,
artist_id TEXT REFERENCES public.artists(id) ON DELETE SET NULL,
slug TEXT UNIQUE,
created_at TIMESTAMPTZ DEFAULT NOW()
);Key indexes for performance optimization:
CREATE INDEX idx_artists_slug ON public.artists(slug);
CREATE INDEX idx_songs_slug ON public.songs(slug);
CREATE INDEX idx_songs_artist_id ON public.songs(artist_id);
CREATE INDEX idx_albums_artist_id ON public.albums(artist_id);
CREATE INDEX idx_news_slug ON public.news(slug);
CREATE INDEX idx_videos_slug ON public.videos(slug);Retrieve all artists, sorted by newest first.
Response:
[
{
"id": "1234567890",
"name": "Artist Name",
"bio": "Artist biography",
"image": "https://...",
"genreIds": ["genre1", "genre2"],
"isFeatured": true,
"slug": "artist-name"
}
]Retrieve all songs with metadata.
Response:
[
{
"id": "1234567890",
"title": "Song Title",
"artistId": "artist123",
"albumId": "album123",
"genreIds": ["genre1"],
"featuredArtistIds": ["artist456"],
"albumArt": "https://...",
"downloadUrl": "https://...",
"duration": "3:45",
"description": "Song description",
"slug": "song-title"
}
]Retrieve all available genres.
Retrieve all albums.
Retrieve all news articles, sorted by creation date.
Retrieve all videos, sorted by newest first.
Secure proxy for file downloads.
Query Parameters:
url(required): File URL to downloadtitle(optional): Filename for downloadartist(optional): Artist name for metadata
All write operations require service role authentication via SUPABASE_SERVICE_ROLE_KEY.
Create a new artist.
Request Body:
{
"id": "unique-id",
"name": "Artist Name",
"bio": "Biography text",
"image": "https://...",
"genreIds": ["genre1", "genre2"],
"isFeatured": false
}Update an existing artist.
Delete an artist by ID.
Similar CRUD operations exist for:
/api/songs/api/albums/api/genres/api/news/api/videos
Authenticate admin user.
Request Body:
{
"password": "admin-password"
}Response:
{
"success": true
}Clear admin session.
-
Row Level Security (RLS)
- All tables have RLS enabled
- Public read access for GET operations
- Authenticated write access for mutations
- Service role bypass for admin operations
-
Input Validation
- Server-side validation for all inputs
- Type checking with TypeScript
- Length limits on text fields
- File type and size validation
-
SQL Injection Prevention
- Parameterized queries via Supabase client
- No raw SQL from user inputs
- Prepared statements for all operations
-
File Upload Security
- MIME type validation
- File size limits (50MB audio, 10MB images)
- Virus scanning (via Supabase)
- Direct browser-to-storage uploads
-
Environment Variables
- Sensitive data in environment variables
- Never exposed to client bundle
- Service role key server-side only
-
HTTPS Only
- All API calls over HTTPS
- Secure cookie flags in production
- HSTS headers enabled
Admin routes are protected by middleware:
// middleware.ts
export function middleware(request: NextRequest) {
const isAdminRoute = request.nextUrl.pathname.startsWith('/admin')
const isAdminApi = request.nextUrl.pathname.startsWith('/api/admin')
if (isAdminRoute && !isAdminApi) {
const hasAuth = request.cookies.get('admin-auth')
if (!hasAuth) {
return NextResponse.redirect(new URL('/admin', request.url))
}
}
}The application uses a direct upload strategy to bypass server limitations:
-
Client-Side Upload
- Browser creates Supabase client with anon key
- File selected via UI component
- Direct upload to Supabase Storage
- Progress tracking and error handling
-
Metadata Extraction
- Audio metadata extracted in parallel
- Duration, format, and quality detection
- Album art extraction from ID3 tags
-
Database Update
- Public URL stored in database
- Metadata saved with file reference
- Atomic operations for consistency
Handles audio file uploads with drag-and-drop support.
<MusicFileDropzone
onFileSelected={(url, metadata) => {
setMusicUrl(url)
setDuration(metadata.duration)
}}
/>Combined component for audio and album art uploads.
<MusicAndArtUpload
musicUrl={downloadUrl}
setMusicUrl={setDownloadUrl}
albumArt={albumArt}
setAlbumArt={setAlbumArt}
onMetadataExtracted={(metadata) => {
setDuration(metadata.duration)
}}
/>- Audio Files: 50MB maximum
- Images: 10MB maximum
- Total Storage: Based on Supabase plan
Each dynamic route includes server-side metadata generation:
// app/artists/[id]/layout.tsx
export async function generateMetadata({ params }): Promise<Metadata> {
const artist = await fetchArtist(params.id)
return {
title: `${artist.name} - Emerging African Artist`,
description: artist.bio,
openGraph: {
title: artist.name,
description: artist.bio,
images: [artist.image],
type: 'profile'
},
twitter: {
card: 'summary_large_image',
title: artist.name,
description: artist.bio,
images: [artist.image]
}
}
}SEO-friendly slug-based URLs:
- Artists:
/artists/artist-name - Songs:
/songs/song-title - News:
/news/article-title - Videos:
/videos/video-title - Genres:
/genres/genre-name
Automatic sitemap generation at /sitemap.xml:
// app/sitemap.ts
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const artists = await fetchAllArtists()
const songs = await fetchAllSongs()
return [
...artists.map(artist => ({
url: `${baseUrl}/artists/${artist.slug || artist.id}`,
lastModified: new Date(),
changeFrequency: 'weekly',
priority: artist.isFeatured ? 0.9 : 0.8
})),
...songs.map(song => ({
url: `${baseUrl}/songs/${song.slug || song.id}`,
lastModified: new Date(),
changeFrequency: 'weekly',
priority: 0.7
}))
]
}biztune/
├── app/ # Next.js App Router
│ ├── (routes)/ # Page components
│ ├── api/ # API route handlers
│ └── globals.css # Global styles
├── components/
│ ├── admin/ # Admin dashboard components
│ ├── ui/ # Reusable UI components
│ ├── navigation.tsx # Main navigation
│ └── footer.tsx # Footer component
├── lib/
│ ├── supabaseClient.ts # Supabase client factory
│ ├── data.ts # TypeScript interfaces
│ ├── slug-utils.ts # URL slug generation
│ ├── validation.ts # Input validation utilities
│ └── seo-metadata.ts # SEO helper functions
├── scripts/
│ ├── *.sql # Database migration scripts
│ └── migrate-urls.mjs # URL migration utility
├── public/ # Static assets
├── types/ # TypeScript type definitions
├── middleware.ts # Next.js middleware
├── next.config.mjs # Next.js configuration
├── tailwind.config.ts # Tailwind configuration
└── tsconfig.json # TypeScript configuration
# Start development server
pnpm dev
# Build for production
pnpm build
# Start production server
pnpm start
# Run linting
pnpm lint
# Type check
pnpm type-check
# Update dependencies
pnpm update
# Audit dependencies
pnpm audit-
Create TypeScript Interface
// lib/data.ts export interface NewFeature { id: string name: string // ... other fields }
-
Create Database Table
-- scripts/add_new_feature.sql CREATE TABLE public.new_feature ( id TEXT PRIMARY KEY, name TEXT NOT NULL );
-
Create API Route
// app/api/new-feature/route.ts export async function GET() { // Implementation }
-
Create UI Component
// components/admin/admin-new-feature.tsx export default function NewFeatureManager() { // Implementation }
-
Add to Admin Dashboard
// app/admin/dashboard/page.tsx import NewFeatureManager from '@/components/admin/admin-new-feature'
- TypeScript: Use strict mode, avoid
anywhen possible - Components: Functional components with hooks
- Naming: PascalCase for components, camelCase for functions
- Files: kebab-case for file names
- CSS: Tailwind utility classes, avoid custom CSS
- Imports: Absolute imports using
@/alias
-
Push to GitHub
git push origin main
-
Import to Vercel
- Go to vercel.com
- Click "New Project"
- Import your GitHub repository
- Configure project settings
-
Environment Variables Add all variables from
.env.localin Vercel dashboard:- Settings → Environment Variables
- Add each variable with appropriate scope (Production, Preview, Development)
-
Deploy
- Vercel automatically deploys on push
- Monitor deployment in dashboard
- Check deployment logs for errors
- All environment variables configured
- Supabase RLS policies enabled
- Storage buckets configured as public
- Admin password changed from default
- Database migrations executed
- SSL certificate active
- Custom domain configured (if applicable)
- Error tracking enabled
- Analytics configured
- SEO metadata verified
- Social sharing tested
- Performance audit completed
- Security headers configured
# Production Environment Variables
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
ADMIN_PASSWORD=strong-production-password
NEXT_PUBLIC_SITE_URL=https://your-domain.com
NODE_ENV=productionConfigure automatic backups in Supabase:
- Navigate to Database → Backups
- Enable daily backups
- Set retention period (7-30 days)
- Test restore procedure
Supabase Dashboard:
- Monitor database queries
- Check storage usage
- Review API logs
- Track error rates
Vercel Analytics:
- Page view tracking
- Performance metrics
- Error monitoring
- User analytics
Cause: Storage bucket not public or incorrect MIME type
Solution:
- Verify bucket is public in Supabase Storage settings
- Check MIME type restrictions
- Verify file size within limits
- Check browser console for specific errors
Cause: Incorrect Supabase credentials or network issue
Solution:
- Verify
NEXT_PUBLIC_SUPABASE_URLis correct - Check
NEXT_PUBLIC_SUPABASE_ANON_KEYmatches project - Ensure Supabase project is active
- Check network connectivity
Cause: Incorrect password or missing environment variable
Solution:
- Verify
ADMIN_PASSWORDin.env.local - Restart development server after changes
- Check for typos in password
- Clear browser cookies and retry
Cause: Row Level Security blocking legitimate requests
Solution:
- Run
scripts/enable_rls_policies.sql - Verify service role key is set correctly
- Check policy definitions in Supabase dashboard
- Review policy logs for specific denials
Cause: Server-side metadata not generated or cached
Solution:
- Verify layout.tsx files exist for dynamic routes
- Test with Open Graph Debugger
- Clear social media cache
- Rebuild and redeploy application
Enable verbose logging:
# .env.local
DEBUG=true
LOG_LEVEL=verboseCheck logs:
- Browser console for client errors
- Vercel deployment logs for server errors
- Supabase logs for database queries
-
Fork Repository
git clone https://github.com/your-username/biztune.git cd biztune git remote add upstream https://github.com/ochudi/biztune.git -
Create Feature Branch
git checkout -b feature/your-feature-name
-
Make Changes
- Follow code style guidelines
- Add tests if applicable
- Update documentation
-
Commit Changes
git add . git commit -m "feat: add new feature description"
Commit message format:
feat:New featurefix:Bug fixdocs:Documentation changesstyle:Code style changesrefactor:Code refactoringtest:Test additions/changeschore:Build process or auxiliary tool changes
-
Push and Create PR
git push origin feature/your-feature-name
Then create Pull Request on GitHub
All pull requests require:
- Passing CI checks
- Code review approval
- Updated documentation
- No merge conflicts
MIT License
Copyright (c) 2025 BizTune
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Documentation Version: 1.0.0
Last Updated: November 2025
Maintainer: Chukwudi Ofoma