Building a Music SaaS
Let's explore the process of building a music Software as a Service (SaaS) platform that enables creators to engage their audience by allowing them to vote on the music played during live streams.
Building a Music SaaS
In this article, we will explore the process of building a music Software as a Service (SaaS) platform that enables creators to engage their audience by allowing them to vote on the music played during live streams. This comprehensive examination will cover both the conceptual framework and technical implementation involved in developing such a platform.
We will begin with an overview of the Music SaaS architecture
, breaking it down into essential components such as user authentication
, stream management
, and audience interaction
features. Following this, we will delve into the technical aspects of implementing these components using modern technologies, including database management
, API integrations
, and real-time streaming
capabilities.
For those who prefer visual learning, a video presentation of this article's content is available on YouTube at
The article will cover everything from user experience design and security considerations to video playback and voting mechanisms, offering valuable insights into the infrastructure and workflows that underpin successful music streaming platforms. By understanding these elements, developers will be better equipped to create engaging and interactive experiences that meet the needs of users in the evolving landscape of digital music consumption.
To follow along with the code examples in this lecture, please refer to the GitHub repository https://github.com/code100x/muzer/commit/76be1650935af46e4d92fee69ef892b4bd98a3e0
Introduction to the Music SaaS Platform Project
Context and Motivation
The following blog post aims to provide a comprehensive overview of the process involved in developing a music SaaS (Software as a Service) platform. This project was initiated with the goal of rebuilding a system that was originally developed in college. The primary motivations behind this endeavor were twofold:
Live Streaming: The creator plans to engage in live streaming activities, and this platform will become essential for selecting music during these events.
Super 30 Cohort: The project is also aligned with the upcoming Super 30 cohort, an on-site program where this product will likely be highly useful.
Project Structure and Objectives
The video is divided into nine parts, each focusing on a specific aspect of the development process. Here is a detailed breakdown of the project structure:
Part 0: High-Level Context
This part provides an overview of the entire project, including its objectives and the steps involved in the development process.
Part 1: Initializing the Application
The first part involves initializing the application, which is a relatively straightforward step.
Part 2: Adding Authentication
In this part, the focus is on adding authentication to the application using Next.js.
Part 3: Designing the Schema
The third part is dedicated to designing the schema, which involves understanding the structure of the database.
Part 4: Back End Development
This section covers the back-end development of the application, including setting up the server and handling requests.
Part 5: Front End Development
The front-end development is a critical component, and it took the most time to complete. This part involves creating the user interface and user experience.
Part 6: Polishing and Testing
In this phase, the focus is on adding last-minute polishes to ensure there are no vulnerabilities and to cover as many edge cases as possible.
Part 7: Deployment
The final part involves deploying the application on the internet, making it accessible to users.
Technical Details
Database Schema: The database schema is a crucial aspect of the project. It defines the structure of the database, including tables, columns, and relationships between them.
Authentication: The use of Next.js for authentication ensures that only authorized users can access the application.
Front End Development: The front-end development involves creating a user-friendly interface that interacts with the back-end to provide a seamless user experience.
Polishing and Testing: This phase includes various testing methodologies to ensure the application is stable and secure. It also involves fixing any bugs or issues that arise during testing.
Future Development
Open Issues: The creator has identified a few issues that were not addressed during the initial development phase. These issues are documented in the repository, and contributors are encouraged to pick them up and resolve them.
Community Involvement: The project aims to become a useful platform for creators, and the community is invited to contribute to its development.
Conclusion
The music SaaS platform project is a comprehensive endeavor that involves multiple stages of development, from high-level context to deployment. By understanding the technical details and objectives of each part, developers can better appreciate the complexity and scope of such projects. The project's open nature and community involvement make it an excellent example of collaborative development in the tech industry.
This blog post provides a detailed overview of the music SaaS platform project, covering every aspect mentioned in the transcript segment. It sets the context, explains the motivations, and outlines the technical details involved in each part of the project. The post also highlights the importance of community involvement and future development opportunities.
Part 2: Initializing the Application
Introduction
In the previous section, we discussed the context and motivation behind our music SaaS platform. This section delves into the initial setup of the application, highlighting the key components and technologies involved.
Problem Statement and Inspiration
The idea for our music SaaS platform was inspired by a project from our college days. During this time, we worked on an application called "M" within the SDS Labs campus group. The application was a gated website where a few lab members could choose from various audio or video tracks. These tracks were scripted in-house and stored on a server, not using YouTube as we do today. The website displayed recently played tracks and allowed users to search for specific music tracks using a search bar. Once a track was selected, it would enter a pending queue where members could decide which track to play next. Tracks with more upvotes would be pushed to the top of the queue, ensuring that the most popular tracks were played next.
This collaborative music player was useful for lab sessions where music was played continuously through speakers. However, it had its limitations, such as not everyone enjoying the same type of music. For instance, Punjabi songs might not be everyone's cup of tea, leading to potential conflicts.
Modernizing the Idea
Fast forward to today, we aim to modernize this concept by integrating it into a more sophisticated SaaS platform. The primary goal is to create a platform where users can choose the next song to play during live streaming sessions, such as on YouTube. This would involve users upvoting their preferred tracks, ensuring that the most popular ones get played. Additionally, we plan to introduce a payment system where users can pay to have their chosen song played immediately, even if it's not currently at the top of the queue.
Use Cases
Colleges and University Groups: The platform can be highly useful for college groups and university settings where music is often played in common areas. It provides a democratic way to choose the next song, ensuring that everyone has a say in what gets played.
Live Streaming: During live streaming sessions on platforms like YouTube, users can interact with the stream by choosing the next song to play. This enhances viewer engagement and provides a more interactive experience.
On-Site Development Programs: We are planning to implement this platform in our upcoming on-site development program, Super 30. This program involves a batch of 30 people who will learn and develop for 3-4 hours each day. The music room will be a central part of this program, allowing participants to code while enjoying music chosen democratically by the group.
Technical Requirements
Frontend Framework: We will use Next.js for the frontend development, which is well-suited for building server-side rendered applications with React.
Database Schema: The database schema will need to handle real-time calculations and updates efficiently. This might involve using PostgreSQL with Prisma for database management.
Real-Time Updates: To handle real-time updates, we will need to implement a mechanism for updating the leaderboard in real-time. This could involve using WebSockets or long polling, although we have decided to keep it simple with long polling for now.
Payment Integration: If we decide to implement payment verification, we will need to use Web3.js and Solana wallet adapter for handling cryptocurrency transactions. However, this part is still under consideration and might not be implemented initially.
UI Design: For the UI design, we will use Tailwind CSS and possibly Shad CN to make the components look visually appealing.
API Integration: We will need to integrate with APIs like YouTube and Spotify to handle track selection and ID extraction. Ensuring there are no collisions where the same song is pushed twice is crucial.
Real-Time Leaderboards: Implementing real-time leaderboards is a challenging task, especially if we have a large number of tracks. This might require building something similar to a real-time leaderboard, which is a hard problem to solve.
Conclusion
In this section, we have outlined the initial setup of our music SaaS platform, including its inspiration, use cases, and technical requirements. By understanding these components, we can appreciate the complexity and scope of such projects. The platform's open nature and community involvement make it an excellent example of collaborative development in the tech industry. In the next sections, we will delve deeper into the authentication process, database schema design, backend development, frontend development, and deployment of the application.
Part 3: Initializing the Application
Initializing a Next.js application is a crucial step in the development process. This section will guide you through the process of setting up a new Next.js project, including the necessary commands and configurations.
Step 1: Initializing the Next.js Project
To begin, you need to create a new Next.js project. You can do this using the create-next-app
command line tool. Here are the steps to initialize a new project:
Open Terminal:
Ensure you have Node.js and npm installed on your system. If you're using Windows, consider using Git Bash or the Windows Subsystem for Linux (WSL) for better compatibility with UNIX-specific commands.
Create a New Project Directory:
Navigate to the directory where you want to create your project.
Run the following command to create a new Next.js project:
npx create-next-app@latest my-next-project --use-pnpm --example "<https://github.com/vercel/next-learn/tree/main/basics/learn-starter>"
This command uses the
create-next-app
tool to set up a new Next.js project with the specified template. The-use-pnpm
flag ensures that pnpm is used as the package manager, which can be beneficial for those experiencing issues with npm locally.
Navigate to the Project Directory:
Once the installation is complete, navigate to the newly created project directory:
cd my-next-project
Start the Development Server:
To start the development server, run:
pnpm dev
This command will start the Next.js development server on port 3000. You can verify this by opening
http://localhost:3000
in your browser.
Customizing the Project Setup
When initializing the project, you may be prompted with several options. Here’s a breakdown of the common configurations:
Project Name: You can specify a name for your project.
Use TypeScript: You can choose to use TypeScript for your project.
Use ESLint: You can enable ESLint for code linting.
Use Tailwind CSS: You can enable Tailwind CSS for styling.
Source Directory: You can choose to keep your application code in a
src
directory.App Router: You can use the App Router instead of the Pages Router.
Customize Imports: You can configure the import alias (
@/*
by default).
Cleaning Up the Initial Project
After initializing the project, you may want to clean up the initial files to better understand the structure and start from scratch. Here’s how you can do it:
Remove Default Text:
Open the
pages/index.tsx
file and remove any default text.This will help you start with a clean slate and ensure that you understand how to add content to your pages.
Remove Global Styles:
Open the
styles/global.css
file and remove all styles except for the Tailwind CSS imports.This will help you understand how to use Tailwind CSS for styling your components.
Add a Basic Greeting:
Add a simple
h1
element to thepages/index.tsx
file to verify that the project is set up correctly.
Run the Development Server:
Navigate to the
app
directory and run:
pnpm dev
This will start the development server, and you should see a basic Next.js page with your greeting.
By following these steps, you can ensure that your Next.js project is set up correctly and you are ready to proceed with adding authentication, designing the schema, and other subsequent steps in the development process.
Part 4: Adding Authentication Using Next.js
Introduction to Authentication
Authentication is a crucial aspect of any web application, ensuring that only authorized users can access and interact with the platform. In this section, we will delve into the process of adding authentication to our music platform using Next.js, focusing on integrating Google login.
Setting Up Next.js for Authentication
To begin, we need to add Next.js as a dependency to our project. Since we are using pnpm
, we can run the following command:
pnpm add next
If you are using npm
, you would use:
npm install next
If you are using yarn
, you would use:
yarn add next
Initializing Routes
After adding Next.js, we need to initialize the routes. However, we are not using the Pages router; instead, we are using the new app router. This is evident from the file structure of our project, which includes an app
folder instead of a Pages
folder. The app router is used in Next.js 13 and above, and it requires us to define route handlers.
Hands on Coding Implementation
1] Authentication and AppBar Components
Defining Route Handlers
To handle requests to the /api
endpoint, we need to create a specific route handler. This involves creating a folder structure like app/api/[...slug].ts
and defining a handler function inside it. The handler function will catch all requests to the /api
endpoint and handle them accordingly.
Using Google Authentication Provider
Next.js provides various providers for authentication, including Google. To use the Google authentication provider, we need to import it from next-auth/providers/google
.
Set Environment Variables:
Create an
.env
file in the root of your project to store the Google client ID and client secret.
GOOGLE_CLIENT_ID=your-client-id
GOOGLE_CLIENT_SECRET=your-client-secret
1. Authentication Route: app/api/auth/[...nextauth]/route.ts
This file sets up the authentication logic using NextAuth with Google as the provider.
import GoogleProvider from "next-auth/providers/google";
import NextAuth from "next-auth";
import { prismaClient } from "@/app/lib/db";
const handler = NextAuth({
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID ?? "",
clientSecret: process.env.GOOGLE_CLIENT_SECRET ?? ""
})
],
secret: process.env.NEXTAUTH_SECRET ?? "secret",
callbacks: {
async signIn(params) {
if (!params.user.email) {
return false; // Prevent sign-in if email is not available
}
try {
await prismaClient.user.create({
data: {
email: params.user.email,
provider: "Google" // Store the provider information
}
});
} catch (e) {
// Handle any errors that occur during the user creation
}
return true; // Allow sign-in if everything is successful
}
}
});
export { handler as GET, handler as POST };
Key Components Explained:
Providers: The
GoogleProvider
is configured withclientId
andclientSecret
, which are retrieved from environment variables. This allows users to authenticate using their Google accounts.Secret: The
NEXTAUTH_SECRET
is used to encrypt session tokens, ensuring secure communication between the client and server.Callbacks:
signIn: This callback is invoked when a user attempts to sign in. It checks if the user's email is present. If not, it returns
false
, preventing the sign-in. If the email is present, it tries to create a new user in the database using Prisma. If successful, it returnstrue
, allowing the sign-in process to continue.
Exporting Handler: The handler is exported for handling both GET and POST requests, making it versatile for various authentication actions like signing in and signing out.
2. AppBar Component: app/components/Appbar.tsx
The AppBar component provides a user interface for signing in and out of the application.
"use client";
import { signIn, signOut, useSession } from "next-auth/react";
import Link from "next/link";
import { Button } from "@/components/ui/button";
export function Appbar() {
const session = useSession(); // Get session data
return (
<div className="flex justify-between px-20 pt-4">
<div className="text-lg font-bold flex flex-col justify-center text-white">
Muzer
</div>
<div>
{session.data?.user ? (
<Button className="bg-purple-600 text-white hover:bg-purple-700" onClick={() => signOut()}>
Logout
</Button>
) : (
<Button className="bg-purple-600 text-white hover:bg-purple-700" onClick={() => signIn()}>
Signin
</Button>
)}
</div>
</div>
);
}
Key Components Explained:
useSession Hook: This hook retrieves the current session state, which contains user information if logged in.
Conditional Rendering:
If a user is logged in (
session.data?.user
is truthy), a "Logout" button is displayed. Clicking this button will invoke thesignOut
function to log the user out.If no user is logged in, a "Signin" button is shown. Clicking this button invokes the
signIn
function, which triggers the authentication flow.
Styling: The component uses Tailwind CSS classes for styling, ensuring a responsive and visually appealing interface.
3. Providers Component: app/providers.tsx
This component wraps the application in a session provider context.
"use client";
import { SessionProvider } from "next-auth/react";
export function Providers({ children }: { children: React.ReactNode }) {
return <SessionProvider>{children}</SessionProvider>;
}
Key Components Explained:
SessionProvider: This component provides session context to its children, allowing any component within the tree to access session data via hooks like
useSession
.
4. Layout Component: app/layout.tsx
The layout component integrates all parts of the application together.
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import { Providers } from "./provider";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({ children }: Readonly<{ children: React.ReactNode; }>) {
return (
<html lang="en">
<body className={inter.className}>
<Providers>
{children}
</Providers>
</body>
</html>
);
}
Key Components Explained:
Metadata: Defines metadata for SEO purposes.
Root Layout:
Wraps all child components with
<Providers>
, ensuring that session management is available throughout the application.Uses Google Fonts for styling and applies global CSS for consistent design.
These components collectively establish a robust authentication system using NextAuth with Google as a provider while providing an intuitive user interface for managing sessions. The use of Prisma for database interactions ensures that user data is stored securely and efficiently.
2] Database Schemas & Management
This section provides a detailed explanation of the Prisma schema file (prisma/schema.prisma
), the API routes for managing streams (app/api/streams/route.ts
), and the database client setup (app/lib/db.ts
). These components work together to define the data structure and interactions for the music SaaS platform.
1. Prisma Schema: prisma/schema.prisma
The Prisma schema is a crucial part of the application, defining the data models, their relationships, and how they interact with the database.
// This is your Prisma schema file,
// learn more about it in the docs: <https://pris.ly/d/prisma-schema>
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id String @id @default(uuid())
email String @unique
provider Provider
streams Stream[] @relation("user")
upvotes Upvote[]
}
model Stream {
id String @id @default(uuid())
type StreamType
url String
extractedId String
title String @default("")
smallImg String @default("")
bigImg String @default("")
active Boolean @default(true)
played Boolean @default(false)
playedTs DateTime?
createAt DateTime @default(now())
upvotes Upvote[]
userId String
user User @relation(fields: [userId], references: [id], name: "user")
currentStream CurrentStream?
}
model CurrentStream {
userId String @id
streamId String? @unique
stream Stream? @relation(fields: [streamId], references: [id])
}
model Upvote {
id String @id @default(uuid())
userId String
streamId String
user User @relation(fields: [userId], references: [id])
stream Stream @relation(fields: [streamId], references: [id], onDelete: Cascade)
@@unique([userId, streamId])
}
enum StreamType {
Spotify
Youtube
}
enum Provider {
Google
}
Key Components Explained:
Generator:
The
generator client
block specifies that Prisma Client will be generated using JavaScript. This client allows interaction with the database through defined models.
Datasource:
The
datasource db
block defines the database connection. It specifies PostgreSQL as the database provider and retrieves the connection URL from an environment variable (DATABASE_URL
).
Models:
User: Represents users of the application.
id
: Unique identifier for each user.email
: Unique email for authentication.provider
: Indicates which authentication provider (e.g., Google) was used.streams
: A one-to-many relationship with streams created by this user.upvotes
: A one-to-many relationship with upvotes associated with this user.
Stream: Represents a music stream.
id
: Unique identifier for each stream.type
: Specifies whether the stream is from Spotify or YouTube (using theStreamType
enum).url
,extractedId
,title
, etc.: Various attributes related to the stream.userId
: Foreign key linking to the creator of the stream (User).currentStream
: Optional relation to track if this stream is currently active.
CurrentStream: Tracks which stream is currently being played for a specific user.
Contains foreign keys linking to both User and Stream.
Upvote: Represents a user's upvote on a stream.
Contains foreign keys linking to both User and Stream, ensuring that each user can only upvote a specific stream once (enforced by the unique constraint on
[userId, streamId]
).
Enums:
StreamType: Enum defining possible types of streams (Spotify or YouTube).
Provider: Enum defining possible authentication providers (Google).
2. API Route for Streams: app/api/streams/route.ts
To add a stream, we need to create an endpoint that accepts a POST request containing the necessary data. The data should include the creator ID and the URL of the stream. We will use Zod to validate the incoming data and ensure it conforms to our schema.
Imports and Constants
import { prismaClient } from "@/app/lib/db";
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
//@ts-ignore
import youtubesearchapi from "youtube-search-api";
import { YT_REGEX } from "@/app/lib/utils";
import { getServerSession } from "next-auth";
const CreateStreamSchema = z.object({
creatorId: z.string(),
url: z.string()
});
const MAX_QUEUE_LEN = 20;
Imports:
prismaClient
: Used to interact with the database via Prisma ORM.NextRequest
andNextResponse
: Used to handle HTTP requests and responses in Next.js.z
: A validation library (Zod) used for schema validation.youtubesearchapi
: A library for fetching YouTube video details.YT_REGEX
: A regular expression for validating YouTube URLs.getServerSession
: A function to retrieve the current session using NextAuth.
Constants:
CreateStreamSchema
: Defines the expected structure of the request body for creating a stream, requiring acreatorId
and aurl
.MAX_QUEUE_LEN
: Sets a limit on the number of active streams a user can have.
POST Request Handler
export async function POST(req: NextRequest) {
try {
const data = CreateStreamSchema.parse(await req.json());
const isYt = data.url.match(YT_REGEX);
if (!isYt) {
return NextResponse.json({
message: "Wrong URL format"
}, {
status: 411
});
}
const extractedId = data.url.split("?v=")[1];
const res = await youtubesearchapi.GetVideoDetails(extractedId);
const thumbnails = res.thumbnail.thumbnails;
thumbnails.sort((a: {width: number}, b: {width: number}) => a.width < b.width ? -1 : 1);
const existingActiveStream = await prismaClient.stream.count({
where: {
userId: data.creatorId
}
});
if (existingActiveStream > MAX_QUEUE_LEN) {
return NextResponse.json({
message: "Already at limit"
}, {
status: 411
});
}
const stream = await prismaClient.stream.create({
data: {
userId: data.creatorId,
url: data.url,
extractedId,
type: "Youtube",
title: res.title ?? "Cant find video",
smallImg: (thumbnails.length > 1 ? thumbnails[thumbnails.length - 2].url : thumbnails[thumbnails.length - 1].url) ?? "<https://cdn.pixabay.com/photo/2024/02/28/07/42/european-shorthair-8601492_640.jpg>",
bigImg: thumbnails[thumbnails.length - 1].url ?? "<https://cdn.pixabay.com/photo/2024/02/28/07/42/european-shorthair-8601492_640.jpg>"
}
});
return NextResponse.json({
...stream,
hasUpvoted: false,
upvotes: 0
});
} catch(e) {
console.log(e);
return NextResponse.json({
message: "Error while adding a stream"
}, {
status: 411
});
}
}
Key Components Explained:
Request Parsing and Validation:
The request body is parsed as JSON, and the schema is validated using Zod. If validation fails, an error response is returned.
YouTube URL Validation:
The URL is checked against the
YT_REGEX
to ensure it is a valid YouTube link. If it does not match, an error response is returned.
Extracting Video ID:
The video ID is extracted from the URL by splitting it at "?v=".
Fetching Video Details:
The YouTube API is called to get details about the video using the extracted ID. This includes information like title and thumbnails.
Thumbnail Handling:
Thumbnails are sorted by width to select appropriate images for display.
Active Stream Count Check:
The current number of active streams for the creator is counted. If this exceeds the maximum allowed (
MAX_QUEUE_LEN
), an error response is returned.
Creating a New Stream:
A new stream entry is created in the database using Prisma Client with relevant details such as user ID, URL, video title, and thumbnail images.
Successful Response:
If successful, a JSON response containing the newly created stream data is returned, along with default values for upvotes and whether the user has upvoted.
GET Request Handler
export async function GET(req: NextRequest) {
const creatorId = req.nextUrl.searchParams.get("creatorId");
const session = await getServerSession();
// TODO: You can get rid of the db call here
const user = await prismaClient.user.findFirst({
where: {
email: session?.user?.email ?? ""
}
});
if (!user) {
return NextResponse.json({
message: "Unauthenticated"
}, {
status: 403
});
}
if (!creatorId) {
return NextResponse.json({
message: "Error"
}, {
status: 411
});
}
const [streams, activeStream] = await Promise.all([
await prismaClient.stream.findMany({
where: {
userId: creatorId,
played: false
},
include: {
_count: {
select: {
upvotes: true
}
},
upvotes: {
where: {
userId: user.id
}
}
}
}),
prismaClient.currentStream.findFirst({
where: {
userId: creatorId
},
include: {
stream: true
}
})
]);
return NextResponse.json({
streams: streams.map(({_count, ...rest}) => ({
...rest,
upvotes: _count.upvotes,
haveUpvoted: rest.upvotes.length ? true : false
})),
activeStream
});
}
Key Components Explained:
Query Parameters:
The
creatorId
is retrieved from the query parameters of the request URL.
Session Handling:
The session is retrieved using
getServerSession()
. If there is no authenticated user, a 403 Forbidden response is returned.
Error Handling:
If no
creatorId
is provided in the request, an error response with status code 411 is returned.
Fetching Streams:
Two asynchronous calls are made using
Promise.all()
to fetch both unplayed streams associated with the creator and the current active stream.
Data Inclusion:
For each stream fetched, it includes counts of upvotes and checks if the current user has upvoted each stream.
Response Formatting:
The response JSON includes all relevant stream details along with additional computed properties like total upvotes and whether the user has upvoted each stream.
The API route defined in
app/api/streams/route.ts
provides essential functionality for managing music streams in the application. It allows users to create new streams by validating YouTube URLs and storing relevant metadata in a database. Additionally, it retrieves existing unplayed streams and their associated data based on user authentication. This structure facilitates robust interaction between users and their music streams, enhancing overall engagement within the platform.
3. Database Client Setup: app/lib/db.ts
This file initializes Prisma Client for database interactions.
import { PrismaClient } from "@prisma/client";
export const prismaClient = new PrismaClient();
// this isn't the best; we should introduce a singleton here
Key Components Explained:
Prisma Client Instance:
An instance of
PrismaClient
is created to interact with the database. This instance can be used throughout the application to perform CRUD operations on defined models.
Singleton Note:
The comment suggests that implementing a singleton pattern would be beneficial to prevent multiple instances of Prisma Client from being created, which can lead to performance issues in serverless environments.
The combination of these files establishes a robust backend architecture for managing users, streams, and upvotes within the music SaaS platform. The schema defines clear relationships between entities, while API routes provide endpoints for creating and retrieving data efficiently. The use of validation libraries like Zod enhances input safety, ensuring that only valid data interacts with the database.
3] Downvote Upvote Stream Routes
This section provides a detailed explanation of the API routes for handling upvotes and downvotes on streams in the music SaaS platform. These routes interact with the database to manage user interactions with streams, specifically allowing users to upvote or downvote a stream.
1. Downvote Route: app/api/streams/downvote/route.ts
This file handles the logic for removing an upvote from a stream.
import { prismaClient } from "@/app/lib/db";
import { getServerSession } from "next-auth";
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
const UpvoteSchema = z.object({
streamId: z.string(),
})
export async function POST(req: NextRequest) {
const session = await getServerSession();
// TODO: You can get rid of the db call here
const user = await prismaClient.user.findFirst({
where: {
email: session?.user?.email ?? ""
}
});
if (!user) {
return NextResponse.json({
message: "Unauthenticated"
}, {
status: 403
});
}
try {
const data = UpvoteSchema.parse(await req.json());
await prismaClient.upvote.delete({
where: {
userId_streamId: {
userId: user.id,
streamId: data.streamId
}
}
});
return NextResponse.json({
message: "Done!"
});
} catch(e) {
return NextResponse.json({
message: "Error while upvoting"
}, {
status: 403
});
}
}
Key Components Explained:
Zod Schema:
The
UpvoteSchema
defines the expected structure of incoming requests, requiring astreamId
to identify which stream is being downvoted.
Session Handling:
The session is retrieved using
getServerSession()
, which checks if the user is authenticated. If not, it returns a 403 status with an "Unauthenticated" message.
Database Interaction:
The code attempts to find the user in the database based on their email. This is a point of improvement, as it could be optimized by using session information directly instead of making a database call.
Deleting an Upvote:
If the user is authenticated, the code parses the request body and deletes the corresponding upvote from the database using Prisma Client. The deletion is based on a composite key (
userId_streamId
), ensuring that only the specific upvote by that user for that stream is removed.
Response Handling:
If successful, it returns a success message. If there’s an error (e.g., invalid input), it catches the exception and returns an error message with a 403 status.
2. Upvote Route: app/api/streams/upvote/route.ts
To upvote a stream, we need to create an endpoint that accepts a POST request containing the stream ID. We will use the Prisma client to update the upvotes count for the specified stream.
This file handles adding an upvote to a stream.
import { prismaClient } from "@/app/lib/db";
import { getServerSession } from "next-auth";
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
const UpvoteSchema = z.object({
streamId: z.string(),
})
export async function POST(req: NextRequest) {
const session = await getServerSession();
// TODO: You can get rid of the db call here
const user = await prismaClient.user.findFirst({
where: {
email: session?.user?.email ?? ""
}
});
if (!user) {
return NextResponse.json({
message: "Unauthenticated"
}, {
status: 403
});
}
try {
const data = UpvoteSchema.parse(await req.json());
await prismaClient.upvote.create({
data: {
userId: user.id,
streamId: data.streamId
}
});
return NextResponse.json({
message: "Done!"
});
} catch(e) {
return NextResponse.json({
message: "Error while upvoting"
}, {
status: 403
});
}
}
Key Components Explained:
Zod Schema:
Similar to the downvote route, this schema validates that incoming requests contain a valid
streamId
.
Session Handling:
The session is again retrieved to check if the user is authenticated. If not, it returns a 403 status.
Database Interaction:
After confirming authentication, it attempts to find the user in the database based on their email (again, this could be optimized).
Creating an Upvote:
If validation passes and the user exists, it creates a new entry in the
upvotes
table using Prisma Client. This associates the current user with the specified stream via their IDs.
Response Handling:
Returns a success message upon successful creation or an error message if any issues arise during processing.
Comparison with Stream Management Route (app/api/streams/route.ts
)
The app/api/streams/route.ts
file manages streams by allowing users to create new streams and retrieve existing ones. Here’s how it relates to upvotes and downvotes:
User Interaction:
All three routes (creating streams, upvoting, and downvoting) require authentication to ensure that only logged-in users can perform actions.
The streams route allows users to create new streams, while upvotes and downvotes manage interactions with those streams.
Data Integrity:
Each route interacts with the same underlying data models defined in Prisma (
User
,Stream
,Upvote
). This ensures that changes made by one route are reflected across others.The upvote and downvote routes specifically ensure that users can only interact with streams they have access to, maintaining data integrity through foreign key relationships.
Error Handling:
All routes implement error handling for both authentication failures and operational issues (like invalid input or database errors). This consistency improves robustness and user experience.
Optimizations:
Both upvote routes have a commented note suggesting that unnecessary database calls can be eliminated by leveraging session information directly instead of querying for user details each time.
The upvote and downvote API routes provide essential functionality for managing user interactions with streams in the music SaaS platform. They ensure that users can express their preferences while maintaining robust authentication and error handling practices. Together with the stream management route, they form a cohesive system that enhances user engagement within the platform.
4] Building Landing Page & Components
This section provides a detailed explanation of the landing page (app/page.tsx
) and its related components (app/components/Appbar.tsx
, app/components/Redirect.tsx
, and app/components/StreamView.tsx
). These components work together to create the main user interface and functionality of the music SaaS platform.
In the development of the music platform, the front end plays a crucial role in creating a user-friendly interface. This section will delve into the technical details of designing and implementing the front end using Next.js and Tailwind CSS.
Landing Page: app/page.tsx
The landing page serves as the main entry point for users visiting the application. It showcases the platform's features and encourages users to sign up or learn more about the service.
import Link from "next/link";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Users, Radio, Headphones } from "lucide-react";
import { Appbar } from "./components/Appbar";
import { Redirect } from "./components/Redirect";
export default function LandingPage() {
return (
<div className="flex flex-col min-h-screen bg-gradient-to-br from-gray-900 via-purple-900 to-gray-900">
<Appbar />
<Redirect />
{/* ... */}
</div>
);
}
Key Components Explained:
Background: The page uses a gradient background to create a visually appealing layout.
Appbar: The
Appbar
component is included, providing a consistent navigation bar across the application.Redirect: The
Redirect
component is used to redirect authenticated users to the dashboard page, ensuring they don't see the landing page if they are already logged in.
The landing page is divided into several sections:
Hero Section:
Displays a catchy headline and a brief description of the platform's main feature: letting fans choose the music.
Includes two buttons: "Get Started" and "Learn More" to guide users towards sign-up or learning more about the service.
Key Features Section:
Highlights three key features of the platform: fan interaction, live streaming, and high-quality audio.
Each feature is represented by an icon and a short description.
Sign-Up Section:
Encourages users to sign up for the service by providing an email input field and a "Sign Up" button.
Includes a call-to-action message emphasizing the platform's ability to transform streams.
Footer:
Displays copyright information and links to the terms of service and privacy policy.
Appbar Component: app/components/Appbar.tsx
The Appbar
component provides a consistent navigation bar across the application. It includes the platform's name and handles user authentication.
"use client";
import { signIn, signOut, useSession } from "next-auth/react";
import Link from "next/link";
import { Button } from "@/components/ui/button";
export function Appbar() {
const session = useSession();
return (
<div className="flex justify-between px-20 pt-4">
<div className="text-lg font-bold flex flex-col justify-center text-white">
Muzer
</div>
<div>
{session.data?.user ? (
<Button className="bg-purple-600 text-white hover:bg-purple-700" onClick={() => signOut()}>
Logout
</Button>
) : (
<Button className="bg-purple-600 text-white hover:bg-purple-700" onClick={() => signIn()}>
Signin
</Button>
)}
</div>
</div>
);
}
Key Components Explained:
Conditional Rendering: The Appbar conditionally renders a "Logout" button if the user is logged in or a "Signin" button if the user is not logged in.
Sign-In and Sign-Out: The "Signin" and "Logout" buttons trigger the sign-in and sign-out actions using the
signIn
andsignOut
functions provided by NextAuth.
Redirect Component: app/components/Redirect.tsx
The Redirect
component checks if a user is logged in and redirects them to the dashboard page if they are.
"use client";
import { useSession } from "next-auth/react";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
export function Redirect() {
const session = useSession();
const router = useRouter();
useEffect(() => {
if (session?.data?.user) {
router.push("/dashboard");
}
}, [session]);
return null;
}
Key Components Explained:
Session Data: The component retrieves the session data using the
useSession
hook provided by NextAuth.Redirect Logic: If the user is logged in (
session?.data?.user
), the component uses theuseRouter
hook to redirect them to the "/dashboard" route.Conditional Rendering: The component doesn't render anything visually; it only performs the redirect logic.
StreamView Component: app/components/StreamView.tsx
The StreamView
component is responsible for displaying the current stream and the upcoming songs in the queue. It allows users to vote on songs and adds new songs to the queue.
"use client";
import { useEffect, useRef, useState } from 'react';
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Card, CardContent } from "@/components/ui/card";
import { ChevronUp, ChevronDown, ThumbsDown, Play, Share2, Axis3DIcon } from "lucide-react";
import { toast, ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import { Appbar } from '../components/Appbar';
import LiteYouTubeEmbed from 'react-lite-youtube-embed';
import 'react-lite-youtube-embed/dist/LiteYouTubeEmbed.css';
import { YT_REGEX } from '../lib/utils';
import YouTubePlayer from 'youtube-player';
interface Video {
"id": string,
"type": string,
"url": string,
"extractedId": string,
"title": string,
"smallImg": string,
"bigImg": string,
"active": boolean,
"userId": string,
"upvotes": number,
"haveUpvoted": boolean
}
// ...
The StreamView
component includes various features such as:
Displaying the current video:
Shows the title and thumbnail of the currently playing video.
Embeds the video using the
LiteYouTubeEmbed
component.
Upcoming songs queue:
Displays a list of upcoming songs in the queue.
Allows users to upvote or downvote songs in the queue.
Adding new songs to the queue:
Provides an input field for users to enter a YouTube video URL.
Validates the URL format using a regular expression (
YT_REGEX
).Sends a POST request to the
/api/streams
endpoint to add the new song to the queue.
Automatic playback:
Uses the
YouTubePlayer
library to control the video playback.Automatically plays the next song in the queue when the current one ends.
Refreshing the stream data:
Periodically fetches the latest stream data from the
/api/streams
endpoint using therefreshStreams
function.Updates the queue and the currently playing video based on the fetched data.
Sharing the stream link:
Provides a "Share" button that copies the shareable link to the clipboard.
Uses the
toast
library to display a success or error message based on the copy operation's result.
The landing page, Appbar, Redirect, and StreamView components work together to create an engaging user experience for the music SaaS platform. The landing page introduces the platform's features and encourages sign-ups, while the Appbar and Redirect components handle authentication and navigation. The StreamView component is the heart of the application, allowing users to interact with the live stream by adding songs, voting on them, and sharing the stream with others.
5] Building Dashboard Page, Utils, Finishing API Routes
This section provides a detailed explanation of the dashboard page (app/dashboard/page.tsx
) and its related files (app/api/streams/my/route.ts
, app/lib/utils.ts
, app/creator/[creatorId]/page.tsx
, and app/api/streams/next/route.ts
). These components work together to create the main user interface and functionality of the music SaaS platform's dashboard.
Dashboard Page: app/dashboard/page.tsx
The dashboard page serves as the main interface for users to manage their streams and interact with the live stream.
"use client";
import { useEffect, useState } from 'react';
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Card, CardContent } from "@/components/ui/card";
import { ChevronUp, ChevronDown, ThumbsDown, Play, Share2 } from "lucide-react";
import { toast, ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import { Appbar } from '../components/Appbar';
import LiteYouTubeEmbed from 'react-lite-youtube-embed';
import 'react-lite-youtube-embed/dist/LiteYouTubeEmbed.css';
import { YT_REGEX } from '../lib/utils';
import StreamView from '../components/StreamView';
interface Video {
"id": string,
"type": string,
"url": string,
"extractedId": string,
"title": string,
"smallImg": string,
"bigImg": string,
"active": boolean,
"userId": string,
"upvotes": number,
"haveUpvoted": boolean
}
const REFRESH_INTERVAL_MS = 10 * 1000;
const creatorId = "3ce10574-0396-43ac-8274-02882cde607b"
export default async function Component() {
try {
const data = await fetch("/api/user").then(res => res.json());
return <StreamView creatorId={data.user.id} playVideo={true} />
} catch(e) {
return null
}
}
export const dynamic = 'auto'
Key Components Explained:
Fetching User Data: The dashboard page fetches the user's data from the
/api/user
endpoint using thefetch
function. This data is used to retrieve the user's streams and display them in theStreamView
component.Rendering StreamView: The
StreamView
component is rendered with the user's ID and theplayVideo
prop set totrue
. This allows the dashboard to display the current stream and the upcoming songs in the queue.Dynamic Rendering: The
export const dynamic = 'auto'
statement ensures that the page can be rendered dynamically, allowing for efficient rendering of theStreamView
component.
API Route for User's Streams: app/api/streams/my/route.ts
This file handles API requests related to retrieving a user's streams.
import { prismaClient } from "@/app/lib/db";
import { getServerSession } from "next-auth";
import { NextRequest, NextResponse } from "next/server";
export async function GET(req: NextRequest) {
const session = await getServerSession();
// TODO: You can get rid of the db call here
const user = await prismaClient.user.findFirst({
where: {
email: session?.user?.email ?? ""
}
});
if (!user) {
return NextResponse.json({
message: "Unauthenticated"
}, {
status: 403
});
}
const streams = await prismaClient.stream.findMany({
where: {
userId: user.id
},
include: {
_count: {
select: {
upvotes: true
}
},
upvotes: {
where: {
userId: user.id
}
}
}
});
return NextResponse.json({
streams: streams.map(({_count, ...rest}) => ({
...rest,
upvotes: _count.upvotes,
haveUpvoted: rest.upvotes.length ? true : false
}))
});
}
Key Components Explained:
Authentication: The route checks if the user is authenticated by retrieving the session data using
getServerSession()
. If the user is not authenticated, it returns a 403 Forbidden response.Fetching Streams: If the user is authenticated, the route fetches all the streams associated with the user's ID using Prisma Client's
findMany()
method. It includes the count of upvotes for each stream and checks if the current user has upvoted each stream.Response: The route returns a JSON response containing the user's streams with additional information like the number of upvotes and whether the current user has upvoted each stream.
Utility Functions: app/lib/utils.ts
This file contains utility functions and constants used throughout the application.
Using Regular Expressions for URL Validation
Regular expressions can be used to validate URLs and extract specific parts of them. For example, to check if a URL is a valid YouTube URL, we can use the following regular expression:
const youtubeRegex = /^.*(?:youtu.be\\\\/|v\\\\/|u\\\\/\\\\w\\\\/|embed\\\\/|watch\\\\?v=)([^#&?]*)(?:\\\\&.*+)?$/.test(data.url);
This regular expression checks if the URL contains 'youtube' and extracts the video ID.
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export const YT_REGEX = /^(?:(?:https?:)?\\\\/\\\\/)?(?:www\\\\.)?(?:m\\\\.)?(?:youtu(?:be)?\\\\.com\\\\/(?:v\\\\/|embed\\\\/|watch(?:\\\\/|\\\\?v=))|youtu\\\\.be\\\\/)((?:\\\\w|-){11})(?:\\\\S+)?$/;
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
Key Components Explained:
YT_REGEX: A regular expression pattern used to validate YouTube video URLs.
cn Function: A utility function that combines multiple class names using the
clsx
andtailwind-merge
libraries. This function helps with conditional styling and merging class names.
Creator Page: app/creator/[creatorId]/page.tsx
This file defines the page for displaying a specific creator's stream.
import StreamView from "@/app/components/StreamView";
export default function Creator({
params: {
creatorId
}
}: {
params: {
creatorId: string;
}
}) {
return <div>
<StreamView creatorId={creatorId} playVideo={false} />
</div>
}
Key Components Explained:
Rendering StreamView: The
StreamView
component is rendered with thecreatorId
parameter passed from the URL. TheplayVideo
prop is set tofalse
to prevent automatic playback of the stream.Accessing URL Parameters: The
params
object is used to access thecreatorId
parameter from the URL.
API Route for Next Stream: app/api/streams/next/route.ts
This file handles API requests related to retrieving the next stream to be played.
import { prismaClient } from "@/app/lib/db";
import { getServerSession } from "next-auth";
import { NextResponse } from "next/server";
export async function GET() {
const session = await getServerSession();
// TODO: You can get rid of the db call here
const user = await prismaClient.user.findFirst({
where: {
email: session?.user?.email ?? ""
}
});
if (!user) {
return NextResponse.json({
message: "Unauthenticated"
}, {
status: 403
});
}
const mostUpvotedStream = await prismaClient.stream.findFirst({
where: {
userId: user.id,
played: false
},
orderBy: {
upvotes: {
_count: 'desc'
}
}
});
await Promise.all([prismaClient.currentStream.upsert({
where: {
userId: user.id
},
update: {
userId: user.id,
streamId: mostUpvotedStream?.id
},
create: {
userId: user.id,
streamId: mostUpvotedStream?.id
}
}), prismaClient.stream.update({
where: {
id: mostUpvotedStream?.id ?? ""
},
data: {
played: true,
playedTs: new Date()
}
})])
return NextResponse.json({
stream: mostUpvotedStream
});
}
Key Components Explained:
Authentication: The route checks if the user is authenticated by retrieving the session data using
getServerSession()
. If the user is not authenticated, it returns a 403 Forbidden response.Fetching Next Stream: If the user is authenticated, the route fetches the most upvoted unplayed stream associated with the user's ID using Prisma Client's
findFirst()
method. It orders the streams by the count of upvotes in descending order.Updating Current Stream: The route uses Prisma Client's
upsert()
method to update or create the current stream for the user. It sets thestreamId
to the ID of the most upvoted unplayed stream.Updating Stream Status: The route updates the played status of the most upvoted unplayed stream using Prisma Client's
update()
method. It sets theplayed
flag totrue
and theplayedTs
to the current timestamp.Response: The route returns a JSON response containing the most upvoted unplayed stream.
The dashboard page, API routes, utility functions, and creator page work together to provide a comprehensive user interface for managing streams and interacting with live streams. The dashboard fetches the user's streams and displays them using the
StreamView
component. The API routes handle authentication and CRUD operations for streams, while utility functions provide helper functions and constants. The creator page allows users to view a specific creator's stream without automatic playback.
Technical Details
Database Schema: Uses PostgreSQL with Prisma for efficient real-time calculations and updates.
Authentication: Uses Next.js for secure user access.
Front End Development: Focuses on creating a user-friendly interface with Tailwind CSS.
Real-Time Updates: Implements long polling for real-time updates to the leaderboard.
Payment Integration: Plans to use Web3.js and Solana wallet adapter for handling cryptocurrency transactions, though this is still under consideration.
API Integration: Integrates with APIs like YouTube and Spotify to handle track selection and ID extraction, ensuring no song collisions.
Future Development
Open Issues: Identified issues are documented in the repository for community contribution.
Community Involvement: Encourages community contribution to enhance the platform.