The web is evolving, and Web3 technologies are revolutionizing traditional industries, including video streaming. Platforms like Odysee are leading the charge, offering decentralized alternatives to YouTube and Rumble. Similarly, unlike legacy providers like Google Drive and Dropbox, Sia is transforming data storage, providing a privacy-focused and user-centric approach.
Join us on a journey to build a cutting-edge Web3 movie streaming dApp using NextJs, TypeScript, Tailwind CSS, and Sia Renterd. This tutorial series will guide you in creating a decentralized application that leverages Sia's blockchain technology to ensure user data ownership and privacy.
By the end of this tutorial, you'll gain the expertise to:
Watch the demo video below to see the project in action, and subscribe to our channel for more innovative content like this!
To follow along, ensure you have the following tools installed, and familiarity with the stacks will also enhance your understanding:
This three-part series will cover:
If you prefer watching the entire development process, I recommend watching this playlist, in the playlist, everything that is written here and more are captured in the videos.
With that said, let’s jump into setting up this project.
We'll start by cloning a prepared repository that includes the Sia Renterd docker compose script and the backend and frontend services. Run the following commands:
$ git clone https://github.com/Daltonic/sia_vid_tv
$ cd sia_vid_tv
Now, it's crucial that we switch to our starter branch on this newly cloned GitHub project and run the command below to complete that.
$ git checkout 01_starter_branch
Next, let’s set up the associated environment variable for this Renterd service. Create a .env
file at the root of this project and apply the keys below:
RENTERD_SEED=<RENTERD_SEED_PHRASE>
RENTERD_API_PASSWORD=<YOUR_PREFERED_PASSWORD>
RENTERD_LOG_LEVEL=debug
RENTERD_LOG_DATABASE_LEVEL=error
To get these API keys, you will need to have Sia Renterd installed on your machine; please watch the short video below, which pretty much summarizes it all.
Generate a seed phrase with the Renterd application as seen in the above video, and include it inside your environment variable as instructed in the above video. Replace the password with something you can easily remember.
Next, we need to install Docker by downloading it from the official website if you haven't already. Alternatively, use a free online platform like Gitpod or a VPS to run a Docker instance, if possible. Otherwise, install it on your local computer.
Finally, we can spin up a docker container by running the following docker command at the root of this project. Ensure that the terminal is at the same directory location as this docker-compose.yml
file.
$ docker compose -f "docker-compose.yml" up -d --build
Note the command to pull down the container: $ docker compose -f "docker-compose.yml" down
. Run this when you want to shut down your Docker instance (but not now).
If you performed the above instructions correctly, you should see the interface below when you visit your browser at http://localhost:9880
.
Enter your password (from your environment variable) to log in. Then, follow the configuration procedure in the video below to set up your Sia Renterd instance for file uploads, downloads, and streaming.
The above video starts at the 6:41
minute mark, please stop at the 20:01
mark, this part will visually guide you through the Renterd configuration process.
Take note that the blockchain synchronization process, along with host matching, takes up to 25 min
to be ready, so you will have to be patient with the whole process.
Please create a new bucket on Renterd called vidtv
, where all our files for this project will be stored. If you have executed the above instructions successfully, your Renterd node should be ready for upload and download. See the image below.
Amazing. At this point, our Renterd service is ready to start receiving files, but we need to communicate with it programmatically.
Let's round this part one of this tutorial by having the packages and environment variables set up for the backend and frontend.
Backend Project Setup Perform the following instructions to have the backend service packages installed and ready for further development.
Navigate to the backend directory from a new terminal instance using the following commands:
$ cd backend
$ yarn install #you can also use npm install
$ touch .env #or mannually create it at the root of the backend directory
Next, supply the following information into the environment variables.
SIA_API_BUCKET=vidtv
SIA_API_PASSWORD=<YOUR_PREFERED_PASSWORD>
SIA_API_BASE_URL=http://localhost:9880
ORIGIN=http://localhost:9000
PORT=9000
And now, run $ yarn build && yarn start
to spin up the backend and to also confirm that it's free from any bugs.
Frontend Project Setup Lastly, run the following commands to install the packages associated with the frontend. Afterward, we will run it.
Navigate to the backend directory from a new terminal instance using the following commands:
$ cd frontend
$ yarn install #you can also use npm install
$ touch .env #or mannually create it at the root of the backend directory
Next, supply the following information into the environment variables.
NEXT_PUBLIC_PROJECT_ID=<YOUR_WALLET_CONNECT_ID>
NEXT_PUBLIC_FILE_SERVICE_URL=http://localhost:9000
Sign up and create a project with Walletconnect to get your project ID. After you have supplied the project ID to the environment variable, run $ yarn build && yarn start
to spin up the backend and to also confirm that it's free from any bug.
At this point, you will see the interface below when you visit the browser at http://localhost:3000
.
Next Step Congratulations on reaching this milestone! Proceed to Part 2 to complete the backend service development.
Welcome Back! Please read through Part 1 if you haven't already. Now, let's dive into Part 2: Building the backend service for our web3 movie streaming platform.
We've provided a starter code for the backend, which currently displays a "Welcome" message when you start the server and visit http://localhost:9000
in your browser. Let's build on this foundation.
We currently have these codes in the source directory of the backend. Let me briefly explain them to you.
Utility Files
This folder which can be fully addressed to backend/src/utils
contains two essential files: an HTTP exception handler function and an interface for handling file upload information.
This code defines a custom HttpException
class that extends the built-in JavaScript Error
class, allowing for the creation of error instances with specific HTTP status codes and messages.
https://gist.github.com/Daltonic/bb37e1d4c5f84bc9c10fcbb8c3e19621
This code defines an interface FileUpload
that represents an uploaded file, specifying its properties such as name, data, size, encoding, and more, providing a structured way to handle file uploads in this backend application.
https://gist.github.com/Daltonic/64fb31bf3b19668a17d14f59e087a77e
And then at backend/src
root folder, we have this index.ts
file which sets up an Express.js server with CORS and file upload support, defines a single GET route that returns a "Welcome" message, and handles errors by catching and re-throwing them as custom HttpExceptions, then starts the server on the port 9000 as specified in the environment variables.
https://gist.github.com/Daltonic/8d76c3a212681d7bfe89b0792b0e707f
Now that we've covered the key files, let's create two new files in a services
folder, each serving a distinct purpose in our application.
In the backend/src
folder, make a new folder called services
in this location, this is where we'll create two services:
Let’s create a file named sia.service.ts
at the backend/src/services
folder and follow the steps below to formulate this service.
This code defines a SiaService
class that initializes with environment variables for Sia API settings and an origin URL, providing a foundation for managing interactions with the Sia service. Now, let's supply the rest of the codes for this service.
Uploading Files to Sia Renterd To upload files to the Sia Network, we will need to add these three methods into the class, two will be private whereas one will be public.
This code defines a private method generateRandomString
that generates a random string of a specified length, composed of uppercase and lowercase letters and numbers, using a loop to select characters randomly from a predefined string. We will use it to rename each file uniquely before shipping a file to Renterd.
The above code defines a private method uploadToSiaService
that uploads a file to Sia Renterd using Axios, handling upload progress and errors, and returning the Axios response or throwing an error if the upload fails.
The Renterd endpoints are written in the API documentation which you can check out or watch the video below where I explained how the Sia Renterd API documentation.
Now let’s include the public method which we will later expose as an endpoint in our application.
This code defines a public method uploadFile
that uploads a file by generating a unique identifier, saving the file to a local cache, and then uploading it to the Sia Renterd, returning the file's URL and a success message or throwing an error if the upload fails.
Downloading Files to Sia Renterd To download files to the Sia Network, we will need to add these two methods into the class, one will be private and the other will be public.
This code defines a private method downloadFromSiaService
that retrieves a file from the Sia service, caches it locally, and returns a readable stream of the file, handling errors and returning a 404 image if the file is not found.
Let’s have those response_files available in the backend directory, else we will experience an error calling the 404.png
file. At the backend
directory create another folder called response_files
and copy the following images into it.
Perfect, now let’s complete this file download service. Also add the method below in the SiaService
class.
This code defines a public method downloadFile
that calls the private method downloadFromSiaService
to retrieve a file from the Sia Renterd and returns the readable stream of the retrieved file.
It's time we couple these various methods to their respective endpoints, currently, we have just one, but we will need an additional two for uploading and downloading files. File streaming will also utilize the download endpoint.
Head to the backend/src/index.ts
file and update its content with these codes.
This code sets up an Express.js server with CORS and file upload support, defining three endpoints: a welcome message, file upload to the Sia Network, and file download from the Sia Network, using the SiaService class to handle file operations and HttpException for error handling.
Watch this section of the video below if you require some visual aid, ensure you stop at the 01:50:44 timestamp.
We need to create a cache management service to ensure our server doesn't fill up with unused files by controlling how long files stay in the cache. It’s important to know that the only reason we needed this service is to reduce data latency.
Head to the backend/src/services
folder and create a file called background.service.ts
and add these sequences of code to it.
This code defines a BackgroundService
class that sets up a cache directory and schedules daily jobs using the node-cron
library, initializing the background jobs and logging a confirmation message. Let’s create a method that will be responsible for deleting files older than 7 days in the cache.
Deleting Old File
Add this method to the BackgroundService
class.
This code defines a method called deleteOldFiles
that removes files from a cache directory that are older than 7 days, by reading the directory, checking each file's creation time, removing files that exceed the target time, logging the start and end of the job, and any errors or successful deletions.
Now, let’s write a function that will utilize the node-cron package to schedule when to execute the file deletion.
This code sets up a daily cron job to run the deleteOldFiles
method every day at midnight (00:00) to perform automatic file cleanup.
We also need to update the constructor function to schedule the daily Jobs at the instantiation of the background service class.
Perfect, lastly, let’s add this background operation as part of the server process at initialization. Head to the backend/src/index.ts
file, and update the app listener method to import the background service file.
You should rerun the backend service command using $ yarn build && yarn start
and see a terminal printout like the one in the image below.
If you would rather watch how I coded the entire background service, the video below is for you; just ensure you stop at the 02:16:07 timestamp.
Next Step Congratulations, you are now ready for the final part of this tutorial which is Part 3.
Let's dive into the final part of this tutorial series, where we'll integrate the backend with the Frontend, connecting the pieces to complete the file upload application. We will begin by ensuring that authentications in the Frontend are up and running.
Create a new folder named 'config' in the Frontend directory and add an index file, resulting in the path /frontend/config/index.tsx
. Now, let’s add the following codes to it.
This code sets up a Wagmi configuration for our Web3 application, defining metadata, supported chains, and authentication settings, including wallet and social login options, and stores it in the config
export. We also need to create a context API to keep track of the authentication state.
The Context API
Next, create a new folder named 'context' still in the Frontend directory and add an index file, resulting in the path /frontend/context/index.tsx
. Add the following codes to it.
This code sets up a Web3Modal provider using Wagmi and React Query, configuring the Web3 modal with the project ID and theme variables and wrapping the application in a WagmiProvider and QueryClientProvider.
Updating Layout: Let’s have our application layout updated to include the above configurations. Head to /frontend/app/layout.tsx
and replace its codes with the one below.
The above code sets up the root layout for a Next.js application, including metadata, fonts, styles, and providers for Web3 modal, toast notifications, and layout components like header and footer.
The Login Button
Now, we need to enable the login buttons in the /frontend/app/components/layout/Header.tsx
and /frontend/app/components/shared/Menu.tsx
components, and update their codes using the information below.
This code defines a React component for a navigation bar that includes a logo, navigation links, a custom menu, and a login button that launches a Web3 Modal with a responsive design for different screen sizes.
The following images should pop up as proof that what we have done works when you click on the login button and proceed with your preferred provider, X, Facebook, Google, Discord, or Ethereum.
Superb, let’s go deeper and set up our database and NextJs API-based system. For any confusion on the process, please watch the video section below; just make sure you stop at the 02:57:59 mark.
First, let’s update the NextJs configuration script to properly address our pages and endpoints and free our remote images from warnings and scrutiny.
This code defines a Next.js configuration object that sets up API route rewrites and image optimization, allowing remote images from any HTTPS hostname and local images from the localhost domain.
Database Config Script We will be using SQLite for this application, but you are free to use a more robust solution such as MYSQL or NOSQL providers. For the sake of simplicity, let's work with a SQLite flat file.
Create /frontend/app/api/database.ts
file path and add the codes below to it.
This code sets up an SQLite database connection, defines two API functions, apiGet
and apiPost
, to perform GET and POST requests on the database, with error handling and promise-based asynchronous execution. We will be using these codes whenever we wish to send or retrieve data from the database.
Database Migration Script
We need to create both a database flat file and a table to hold all our contents. Create /frontend/app/api/migrations.ts
file path and add the codes below to it.
This code defines a database migration function that creates a 'movies' table with specified columns if it doesn't exist, using SQLite, and logs the result of the operation. Now run the command below in a terminal pointed at the /frontend
directory.
$ cd frontend
$ npx esrun app/api/migrations.ts
It should be noted that this process will also create a database flat file called movies.db
at the root of the frontend directory. We have also added this command to the package.json script, so running $ yarn migrate
on the frontend directory should work the same.
For visual assistance, watch the video below, just stop it at the 03:10:54 mark.
Now, let’s define some endpoints for creating, reading, updating, and deleting movies. We will be using the NextJs API provision to make these endpoints.
Create Movie Endpoint
To create a movie, the required information includes the user ID, movie name, image, video URL, release date, genre, rating, language, duration, and background description. Create /frontend/app/api/movies/create/route.ts
file path and add the codes below to it.
This code defines an endpoint to handle POST requests, validate and process movie data, generate a unique slug, and insert the data into a database using an apiPost function while handling errors and returning JSON responses.
Update Movie Endpoint
To update a movie, the required information includes the user ID, slug, and other information provided when creating a movie. Create /frontend/app/api/movies/update/route.ts
file path and add the codes below to it.
This code defines an endpoint to handle POST requests for updating a movie, validating required properties, and executing an SQL query to update the movie data in the database using the apiPost function.
Delete Movie Endpoint
To delete a movie, the required information includes the user ID and slug of a movie. Create /frontend/app/api/movies/delete/route.ts
file path and add the codes below in it.
This code defines an endpoint to handle POST requests for deleting a movie, validating required properties (userId and slug), and executing an SQL query to delete the movie from the database using the apiPost function.
All Movies Endpoint
The optional data required to get movies are pageSize and userId, which can be passed as query parameters to filter and paginate the results. Create /frontend/app/api/movies/all/route.ts
file path and add the codes below to it.
The above code defines an endpoint to handle GET requests for retrieving movies, allowing optional filtering by userId and pagination by pageSize, and returns the results in JSON format.
Single Movie Endpoint
To retrieve a single movie, the required data is the slug of a movie. Create /frontend/app/api/movies/[slug]/route.ts
file path and add the codes below to it.
This code defines an endpoint to handle GET requests for retrieving a movie by its slug, validating the slug parameter, and executing an SQL query to retrieve the movie data from the database using the apiGet function.
That marks all the endpoints we will need for this application. If you need a visual aid to help you understand these endpoints better, please watch the video below, just ensure you stop at the 03:48:22 timestamp.
Our task is to review and update pre-coded components and pages, explaining each one's purpose and functionality and documenting the changes we make to the existing code. We will start by creating a service for interacting with the endpoints we previously created in the api
directory.
Create /frontend/app/services/api.service.ts
file path and add the codes below to it.
This service provides a set of functions to interact with a movie database, allowing the application to fetch movies, fetch a single movie by slug, create a new movie, update an existing movie, delete a movie, and upload files using API requests and handling errors.
Let’s review and update the various pages associated with our application. You wouldn’t need to change many things, just the ones highlighted here.
Create Movie Page
This page is a movie publishing form that allows users to upload video and image files, input movie details, and submit the form to publish the movie, with validation and error handling, using React and Wagmi libraries.
Now, update the file found in /frontend/app/pages/create/page.tsx
with the codes below.
The changes made in this code compared to the original one are:
createMovie
function from api.service
and used it in the handleSubmit
function to create a new movie.userId
parameter to the createMovie
function call, passing the user's address from the useAccount
hook.handleSubmit
function to use toast.promise
to handle the promise returned by createMovie
.createMovie
function call in the handleSubmit
function.These changes enable the form to submit movie data to the API and create a new movie entry while also handling errors and displaying a success message.
Edit Movie Page
This movie editing page allows authorized users to update movie details, upload posters and videos, and save changes, with validation and error handling, utilizing React, Wagmi, and Next.js, specifically designed for users to edit their movies.
Now, update the file found in /frontend/app/pages/movies/edit/[slug]/page.tsx
with the codes below.
The upgrades made to the code that is different from the original are:
fetchMovie
and updateMovie
functions from @/app/services/api.service
and used them in the useEffect
hook and handleSubmit
function, respectively.posters.find()
method with the fetchMovie
function to retrieve movie data.handleSubmit
function to call the updateMovie
function with the updated movie details.updateMovie
function call in the handleSubmit
function.These changes enable our application to interact with our API endpoints to retrieve and update movie data, whereas the original code relied on our local posters
array.
Home Page
This home page renders the banners component, a list of movies (either from an API source or a loading UI), and subscription options, utilizing React and Next.js, to provide an engaging and informative landing page for users.
Update the file found in /frontend/app/pages/page.tsx
with the following codes.
The changes we made to the home page are:
fetchMovies
function from ./services/api.service
and used it in the useEffect
hook to retrieve movie data from our API.posters
data with the fetchMovies
function call, which fetches data from our API.await
keyword to wait for the promise returned by fetchMovies
to resolve before setting the movies
state.These changes help our application to retrieve movie data from our API instead of relying on local data, making the application more dynamic and data-driven.
User Account Page
This page displays a list of movies posted by the currently connected user, with a loading skeleton placeholder while the data is being fetched and a message prompting the user to connect their account if they haven't done so, utilizing Wagmi and react-loading-skeleton.
Update the file found in /frontend/app/pages/account/page.tsx
with the following codes.
The changes made to the page are:
fetchMovies
function from @/app/services/api.service
and used it in the useEffect
hook to retrieve movie data from our API.posters
data with the fetchMovies
function call, which fetches data from our API.address
as an argument to the fetchMovies
function to retrieve user-specific movie data.address
before rendering the movie list, as the fetchMovies
function now handles this logic.loaded
state.These changes retrieve movie data from our API, specific to the connected user, and display a loading skeleton while the data is being fetched.
Movies Details Page
This page displays a single movie's details, including its name, release year, rating, duration, genre, and background information, along with a video player and related movies, and provides options to edit or delete the movie if the user is the owner, utilizing Next.js, and Wagmi.
Update the file found in /frontend/app/pages/movies/[slug]/page.tsx
with the following codes.
We made some huge changes here! Here's a summary of what we did:
deleteMovie
, fetchMovie
, and fetchMovies
functions from @/app/services/api.service
and used them to interact with our API endpoints.deleteMovie
function.toast.promise
to display a notification while deleting a movie.posters
local data and replaced it with API calls.handleSubmit
function to call the deleteMovie
function and handle the response.useEffect
hook to call the fetchMovie
and fetchMovies
functions.These changes cause our application to interact with our API to retrieve and delete movie data and display notifications to the user during the deletion process.
This part of the video below will show you hands-on how we integrated these pages with the endpoint, please feel free to watch that part if you run into any problem. Just make sure you stop at the 04:57:41 timestamp.
Let’s discuss the purpose of each component in our application. We will update any component that needs to be modified.
Banner Component
This component displays a rotating background image of movie banners, cycling through an array of movie images every 5 seconds, creating a simple and automatic slideshow effect. This component code can be assessed at /frontend/app/components/home/Banner.tsx
.
Posters Component
This component displays a responsive and interactive carousel of movie posters using the Swiper library, with features like autoplay, pagination, and navigation, showcasing a list of movies passed as a prop, with a dynamic layout adapting to different screen sizes. This component code can be assessed at /frontend/app/components/home/Posters.tsx
.
Poster UI Component
This component displays a placeholder skeleton layout for a movie posters section, using the react-loading-skeleton library, showing a dynamic number of skeleton posters based on the "posters" prop, with a responsive design adapting to different screen sizes, indicating a loading state until the actual posters data is fetched and displayed. This component code can be assessed at /frontend/app/components/home/PosterUI.tsx
.
Subscriptions Component
This component displays a subscription plans section, showcasing various dummy plans with their details, prices, and benefits. It allows users to choose a plan that suits their needs, utilizing a responsive grid layout and interactive hover effects to enhance the user experience. This component code can be assessed at /frontend/app/components/home/Subscription.tsx
.
Header Component
This component renders a fixed navigation bar at the top of the page, featuring a logo, a navigation menu with links to various sections, a menu toggle button for responsive design, and a login button, providing a consistent and accessible header section across the application. This component code can be assessed at /frontend/app/components/layout/Header.tsx
.
Footer Component
This component renders a footer section at the bottom of the page, featuring the application's logo, a brief description, navigation links, contact information, and a credit mentioning the decentralized storage solution powered by Sia Foundation, providing a clear and organized footer section with relevant information and links. This component code can be assessed at /frontend/app/components/layout/Footer.tsx
.
Menu Component
This component renders a responsive menu toggle button, which, when clicked, opens or closes a dropdown menu containing navigation links, allowing users to access various sections of the application on smaller screens while hiding the menu on larger screens where the navigation links are already visible. This component code can be assessed at /frontend/app/components/shared/Menu.tsx
.
Movie Card Component
This component displays a single movie's poster with a hover effect, showing additional information such as the movie's name, release year, and background summary while also serving as a link to the movie's details page, utilizing a responsive design and animated transitions to enhance the user experience. This component code can be assessed at /frontend/app/components/shared/MovieCard.tsx
.
Uploaded Component
This component displays a preview of an uploaded file, either an image or a video, with a progress bar and a removal button, allowing users to review and delete the uploaded file, while also providing a visually appealing and interactive interface with animations and hover effects. This component code can be assessed at /frontend/app/components/shared/Uploaded.tsx
.
Uploader Component
This component provides a user interface for uploading files, specifically videos or posters, with features like drag-and-drop, file type validation, size limits, upload progress tracking, and success/error notifications, utilizing a combination of React state management, event handling, and API integration to handle the upload process.
Update the file found in /frontend/app/components/shared/uploader.tsx
with the following codes.
The changes made to this component are:
uploadFile
function from api.service
that handles the file upload.This updated code is more complete and robust, with actual file upload functionality, progress tracking, error handling, and better code organization.
The video below explains what each component does in more detail, kindly check it out for your betterment.
And that is it guys, we have completed this project, and the last step we need to take is to launch this project on the browser. Run $ yarn build && yarn start
to see the project live on the browser.
If you encounter any issues, refer to the following resources for troubleshooting. Till next time, all the best!
I am a web3 developer and the founder of Dapp Mentors, a company that helps businesses and individuals build and launch decentralized applications. I have over 8 years of experience in the software industry, and I am passionate about using blockchain technology to create new and innovative applications. I run a YouTube channel called Dapp Mentors where I share tutorials and tips on web3 development, and I regularly post articles online about the latest trends in the blockchain space.