When building an application with Strapi, you may need to deliver private video content that is accessible only to specific users, such as subscribers or members. Simply hiding video links on the frontend is not secure; users could still access videos if file URLs are public. To ensure proper protection, Strapi provides role-based permissions, custom policies, and storage security mechanisms to keep your videos safe.

Prerequisites

Install and Configure Video Field Plugin

To enable video embedding, install the video field plugin.

Install the Plugin:

With npm:

code
npm install @sklinet/strapi-plugin-video-field

With yarn:

code
yarn add @sklinet/strapi-plugin-video-field

Enable the plugin in config/plugins.js:

code
module.exports = () => ({
"video-field": {
enabled: true
}
});

Run build:

code
npm run build

OR

code
yarn build

Once installed, you"ll see Video Field in the Content-Type Builder. A sample stored value looks like this:

code
{
"provider": "youtube",
"providerUid": "RANDOMUID",
"url": "https://www.youtube.com/watch?v=RANDOMUID"
}
Banner for Video Embedding

Secure Embedded Video Sources

If you plan to embed videos from providers such as YouTube or Vimeo, extend the Content Security Policy (CSP) in config/middlewares.js:

code
module.exports = [
"strapi::errors",
{
name: "strapi::security",
config: {
contentSecurityPolicy: {
useDefaults: true,
directives: {
"frame-src": [
"'self'",
"youtube.com",
"www.youtube.com",
"vimeo.com",
"*.vimeo.com",
"facebook.com",
"www.facebook.com"
],
"media-src": ["'self'", "data:", "blob:"],
},
},
},
},
"strapi::cors",
"strapi::poweredBy",
"strapi::logger",
"strapi::query",
"strapi::body",
"strapi::session",
"strapi::favicon",
"strapi::public",
];

This ensures that only trusted video providers can be embedded in your application.

Configure User Roles and Permissions

Strapi v4 has two types of roles:

Admin Roles: Control access to the Strapi admin panel.

Users & Permissions Plugin Roles: Control access for application users (public, authenticated, custom roles).

For private video content, use the Users & Permissions Plugin Roles.

Steps to Configure:

Step 1: In the Strapi admin panel, go to Settings > Users & Permissions Plugin > Roles.

Step 2: Create a custom role (e.g., Subscriber).

Step 3: Assign permissions for your video content types (Find, FindOne, Create, Update, Delete).

Step 4: Save changes.

Step 5: Assign this role to front-end users either via registration flow or manually in the → Users → section.

Implement Custom Logic for Granular Access

Basic role permissions may not be enough. For example, you may want users to access only the videos they own. Policies let you enforce such rules.

Example Policy: src/policies/isOwner.js

code
module.exports = async (ctx, config, { strapi }) => {
const { user } = ctx.state; // authenticated user
const { id } = ctx.params; // video ID from URL

// Replace 'video' with the name of your content-type
const video = await strapi.db.query('api::video.video').findOne({
where: { id },
});

if (!video || video.owner !== user.id) {
return false; // Access denied
}

return true; // Access granted
};

Apply the policy in your routes file (src/api/video/routes/video.js):

code
module.exports = {
routes: [
{
method: "GET",
path: "/videos/:id",
handler: "video.findOne",
config: {
policies: ["isOwner"],
},
},
],
};

This ensures that only the owner of a video can retrieve it.

Note: In the query above, api::video.video refers to a collection type called → video. → Update this identifier to match your actual content type name.

Securing Video Storage

Permissions and policies protect the API layer, but securing where the actual video files are stored is equally important.

If using Local Uploads, files stored in Strapi"s /public/uploads folder are publicly accessible if requested directly. Instead, move sensitive video files outside the /public directory and serve them through a secured Strapi route with authentication checks.

If using Cloud Storage (e.g., AWS S3), store files as private in the bucket. Generate signed URLs for users to access files temporarily after authentication.

Example: AWS S3 Signed URL Service

code
// src/services/s3.js
const AWS = require("aws-sdk");

const s3 = new AWS.S3({
accessKeyId: process.env.AWS_ACCESS_KEY,
secretAccessKey: process.env.AWS_SECRET_KEY,
region: process.env.AWS_REGION,
});

module.exports = {
getSignedUrl(key) {
return s3.getSignedUrl("getObject", {
Bucket: process.env.AWS_BUCKET,
Key: key,
Expires: 60, // expires in 60 seconds
});
},
};

Example Controller that uses the signed URL:

code
// src/api/video/controllers/video.js
const s3Service = require("../../../services/s3");

module.exports = {
async getSecureUrl(ctx) {
const { id } = ctx.params;
const { user } = ctx.state;

const video = await strapi.db.query('api::video.video').findOne({ where: { id } });

if (!video || video.owner !== user.id) {
return ctx.forbidden("You do not have access to this video.");
}

const url = s3Service.getSignedUrl(video.fileKey);
return { url };
}
};

This ensures that even if someone shares a raw S3 file link, it won"t work, since only signed (expiring) URLs are served to authenticated users.