Creating a video-sharing application like YouTube is not just about front-end design and data storage; you need to have secure dynamic control over what users can see and do. With Svelte.js handling the interface and Firebase supporting backend functionalities, integrating Permit.io enables robust access control, using role-based access control (RBAC) to enforce detailed permissions.
In this tutorial, you will build a secure YouTube clone that allows users to interact only within limited boundaries, according to their role and context.
Building a YouTube Clone With RBAC
Our goal is to create a YouTube clone where permissions control which users can upload, delete, or view videos based on the roles (RBAC) assigned to them.
Create a Firebase App and Get Credentials
Firebase is a hosted platform that helps people to create, run, and scale web and mobile apps. It offers services and tools such as authentication, real-time database, cloud storage, machine learning, remote configuration, and Static file hosting.
We’ll use Firebase for our backend storage and authentication. Follow the steps below to set up your Firebase app:
1. Go to the Firebase Console and create a new project.
2. Enable Firestore Database, Firebase Authentication (Email/Password), and Firebase Storage to manage user data and videos.
3. After setting up your project, navigate to the project settings and add a new Web app. Enter App name as Youtube and click on Register app.
4. From your Firebase console, Click on the Settings icon → Project Settings → Service Account. Select Node.js and click on the “Generate new private key” button to download your credentials.
With Firebase set up, we’re now ready to move on to our app build and integrate Permit.io for managing permissions.
Understanding RBAC and How It Works
Role-based access control (RBAC) is a method of managing user access to systems and resources based on a user’s role or job responsibilities.
With RBAC, permissions are easy to manage for different user types because roles determine what actions can be taken. Here is how it works in our app:
- Roles like Admin, Creator, and Viewer determine which actions users can take.
- Each API route checks the user’s role before allowing access.
Let’s look at this example on the flowchart below, which shows the user roles in our app and what access they have.
Why Use RBAC?
With RBAC, we can give permissions by setting roles such as Admin, Creator, and Viewer and giving each role what they can do. This structure also allows us to easily organize access into broad categories, which also means that if we have to change permissions for a group, it’s a quick role update. For example, Admins can have complete control over videos, Editors can edit the content, and Viewers can only view videos without any editing rights.
To learn more about RBAC and why you should use it in your application, check out this blog.
Setting Up Permit.io and Credentials
To start with Permit.io, you’ll first need to set up an account and get the necessary credentials. These credentials are essential for connecting Permit.io to our backend, where we’ll handle all permission checks.
To do that, follow the steps below:
1. Create a Permit.io account and set up a new project in the dashboard.
2. From your dashboard, click on Projects from the side panel.
3. By default, Permit.io provides you with two environments: Production and Development. Select your preferred environment or create a new one and Copy API Key.
Creating RBAC Policies in Permit.io UI
Now that we understand what RBAC is and how it applies to our YouTube clone app, let’s proceed; let’s create RBAC roles and policies in Permit.io. Follow these steps to set up RBAC:
1. In the Permit.io dashboard, click on Policy → Roles → Add Roles, and create admin
, content_creator
, and viewer
roles.
2. From the policy page, click on Resources → Add Resource, create a video resource, and add the actions: create
, read
, delete
, update
, like
and comment
.
3. Navigate to the Policy Editor and grant each role access to the Video resource.
From the above image, we have given the Admin full access to the Video resource and the Content Creator other access except to delete a video and the Viewer access to comment, create, read, and like a video.
1. After defining permissions for each role, save the policies.
2. Go to the Directory page and click the Add User button to create your first user.
We have created a new user with a Viewer role. This means that this user will have all the access granted to the viewer role on the Video resource.
Integrate Permit.io With the Backend
Now that roles and permissions are defined, it’s time to connect Permit.io to our backend API. To get started quickly, clone the starter project and install dependencies.
git clone https://github.com/icode247/youtube_clone_starter
cd youtube_clone_starter
cd backend && npm install && cd .. && cd frontend && npm install
This project includes the frontend built in Svelte, the backend API in Node.js, and Firebase integrations.
Configuring Permit.io
Next, create a config/permit.js
file in your backend folder and add the Permit configuration:
import { Permit } from "permitio";
const permit = new Permit({
// We’ll use a cloud hosted policy decision point
pdp: "https://cloudpdp.api.permit.io",
token: process.env.PERMIT_API_KEY,
});
export default permit;
Create a .env file in the backend folder, and add the PERMIT_API_KEY
you copied earlier.
Syncing Users With Permit.io
To allow Permit.io to know our users and the roles they have, we need to sync our users with Permit.io at the point of successful registration. In controllers/auth/register.js
file, update the register function with the code below to sync the user and assign a role after registration:
//...
import permit from "../config/permit.js";
export async function register(req, res) {
try {
const { email, password, username, uid } = req.body;
if (!email || !password || !username) {
return res.status(400).json({
error: "Email, password, and username are required",
});
}
await db.collection("users").doc(uid).set({
email,
username,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
});
// Sync user with Permit.io
await permit.api.syncUser({
key: email,
email: email,
first_name: username,
last_name: "",
attributes: {},
});
// Assign default viewer role
await permit.api.assignRole(
JSON.stringify({
user: email,
role: "viewer",
tenant: "default",
})
);
res.status(201).json({
user: {
id: uid,
email: email,
username: username,
},
});
} catch (error) {
console.error("Registration error:", error);
if (error.code === "auth/email-already-exists") {
return res.status(400).json({
error: "Email already in use",
});
}
res.status(500).json({
error: "Registration failed",
});
}
}
//...
In the above code, once a user registers, we sync it with Permit.io and assign it a Viewer role. Let’s register a new user named John Doe in our YouTube clone app and see our implementation in action.
Go back to the Directory → Users tab, and you will see the new user we just created.
Next, update the controllers/channelController.js
file to assign a user a content-creator role when they create a channel so they can create and manage their videos.
//...
export async function createChannel(req, res) {
try {
const { name, description } = req.body;
const avatarFile = req.files?.avatarFile?.[0];
const bannerFile = req.files?.bannerFile?.[0];
let avatarUrl = null;
if (avatarFile) {
const avatarFileName = `channels/${req.user.uid}/avatar_${Date.now()}_${
avatarFile.originalname
}`;
const avatarRef = storage.bucket().file(avatarFileName);
await avatarRef.save(avatarFile.buffer, {
metadata: {
contentType: avatarFile.mimetype,
},
});
const [avatarSignedUrl] = await avatarRef.getSignedUrl({
action: "read",
expires: "03-01-2500",
});
avatarUrl = avatarSignedUrl;
}
let bannerUrl = null;
if (bannerFile) {
const bannerFileName = `channels/${req.user.uid}/banner_${Date.now()}_${
bannerFile.originalname
}`;
const bannerRef = storage.bucket().file(bannerFileName);
await bannerRef.save(bannerFile.buffer, {
metadata: {
contentType: bannerFile.mimetype,
},
});
const [bannerSignedUrl] = await bannerRef.getSignedUrl({
action: "read",
expires: "03-01-2500",
});
bannerUrl = bannerSignedUrl;
}
await db
.collection("channels")
.doc(req.user.uid)
.set({
name,
description,
avatarUrl,
bannerUrl,
userName: name,
createdAt: new Date().toISOString(),
subscribers: 0,
totalViews: 0,
customization: {
theme: "default",
layout: "grid",
},
});
const channelRef = db.collection("channels").doc(req.user.uid);
const channelDoc = await channelRef.get();
//assign a user a new role
await permit.api.assignRole({
user: req.user.email,
role: "content_creator",
tenant: "default",
});
res.status(201).json({
id: channelRef.id,
...channelDoc.data(),
});
} catch (error) {
console.error("Error creating channel:", error);
res.status(500).json({ error: "Failed to create channel" });
}
}
Now, go back to the app and click on the Create Channel button to create a new channel.
So, after creating the channel, a new content_creator
role will be added to the new users.
Set Up Permission Middleware
We’ve able to sync our users with Permit.io and assign them a role, let’s create a middleware to enforce the permissions and role checks we implemented. Create a newpermissions.js
file in the middleware
directory and add the code below to handle permission checks:
import permit from "../config/permit.js";
export const checkPermission = (action, resource) => async (req, res, next) => {
const { email } = req.user;
if (!email) {
return res.status(401).json({ error: "Unauthorized" });
}
try {
const permitted = await permit.check(email, action, resource);
if (!permitted) {
return res.status(403).json({ error: "Forbidden" });
}
next();
} catch (error) {
console.error("Permission check failed:", error);
res.status(500).json({ error: "Internal server error" });
}
};
The checkPermission
function takes two parameters:
- The action to be performed on the Video resource
- The resource on which the action will be performed on
We then used the permit.check
function from Permit.io to check whether the user has permission to access the resource. The first argument passed to the check
function is the user’s key, which we used the user’s email to sync the users with Permit.io, then the action and the resource.
Applying Middleware in Routes
Let’s update all our route files to use the permission middleware for protected routes. We’ll add to the routes/videos.js
file, go ahead and add to other routes.
//...
import { authenticateUser } from "../middleware/auth.js";
//...
router.get("https://dzone.com/", checkPermission("read", "video"), videoController.listVideos);
router.get("/:id", checkPermission("read", "video"), videoController.getVideo);
router.post(
"https://dzone.com/",
checkPermission("create", "video"),
uploadVideo,
videoController.createVideo
);
router.put(
"/:id",
checkPermission("update", "video"),
videoController.updateVideo
);
router.delete(
"/:id",
checkPermission("delete", "video"),
videoController.deleteVideo
);
router.post(
"/:id/like",
checkPermission("like", "video"),
videoController.toggleLike
);
router.get(
"/:id/like",
checkPermission("read", "video"),
videoController.getLikes
);
router.post(
"/:id/comments",
checkPermission("comment", "video"),
videoController.addComment
);
router.get(
"/:id/comments",
checkPermission("read", "video"),
videoController.getComments
);
//...
Conclusion
Implementing RBAC in our YouTube clone has provided a secure and manageable way to control user access based on roles. Permit.io’s simple UI and API make setting up and enforcing role-based permissions trivial, and we can easily assign and change access levels. This foundational layer of access control ensures that every user can interact with the app in their own way, based on their role, making the app ready for real-world use.
For those exploring more advanced, context-sensitive access control, attribute-based access control (ABAC) is a logical next step, offering flexibility for future needs.