Products
Products
Video Hosting
Upload and manage your videos in a centralized video library.
Image Hosting
Upload and manage all your images in a centralized library.
Galleries
Choose from 100+templates to showcase your media in style.
Video Messaging
Record, and send personalized video messages.
CincoTube
Create your own community video hub your team, students or fans.
Pages
Create dedicated webpages to share your videos and images.
Live
Create dedicated webpages to share your videos and images.
For Developers
Video API
Build a unique video experience.
DeepUploader
Collect and store user content from anywhere with our file uploader.
Solutions
Solutions
Enterprise
Supercharge your business with secure, internal communication.
Townhall
Webinars
Team Collaboration
Learning & Development
Creative Professionals
Get creative with a built in-suite of editing and marketing tools.
eCommerce
Boost sales with interactive video and easy-embedding.
Townhall
Webinars
Team Collaboration
Learning & Development
eLearning & Training
Host and share course materials in a centralized portal.
Sales & Marketing
Attract, engage and convert with interactive tools and analytics.
"Cincopa helped my Enterprise organization collaborate better through video."
Book a Demo
Resources
Resources
Blog
Learn about the latest industry trends, tips & tricks.
Help Centre
Get access to help articles FAQs, and all things Cincopa.
Partners
Check out our valued list of partners.
Product Updates
Stay up-to-date with our latest greatest features.
Ebooks, Guides & More
Customer Stories
Hear how we've helped businesses succeed.
Boost Campaign Performance Through Video
Discover how to boost your next campaign by using video.
Download Now
Pricing
Watch a Demo
Demo
Login
Start Free Trial
Video-on-demand platforms require efficient management of large media files, metadata, and secure content delivery. Strapi provides a customizable API and media library. But it doesn’t handle storage, streaming, or frontend rendering directly. Integrating Strapi with external providers (AWS S3, transcoding services, or CDNs) enables scalable storage, adaptive streaming, role-based access, and secure playback, forming a production-ready VOD backend. Prerequisites Install Node.js v18+ using this command to upgrade the Node.js version: Use a package manager: npm, yarn, or pnpm . For production, use PostgreSQL . Setup Project with PostgreSQL PostgreSQL scales better with relational data like users, roles, and video metadata. Create a Strapi Project with PostgreSQL npx create-strapi-app@latest my-vod-cms --quickstart Configure the Database Connection Update ./config/database.js with your PostgreSQL credentials. It ensures reliable handling of metadata, supports concurrency at scale, and keeps sensitive credentials portable across dev, staging, and production via environment variables. module.exports = ({ env }) => ({ connection: { client: 'postgres', connection: { host: env('DATABASE_HOST', '127.0.0.1'), port: env.int('DATABASE_PORT', 5432), database: env('DATABASE_NAME', 'vodcms'), user: env('DATABASE_USERNAME', 'voduser'), password: env('DATABASE_PASSWORD', 'securepassword'), ssl: env.bool('DATABASE_SSL', false), }, }, }); Explanation : host: env('DATABASE_HOST', '127.0.0.1') : Reads the DATABASE_HOST environment variable to determine the database server’s hostname. Defaults to '127.0.0.1' (localhost) if not set. port: env.int('DATABASE_PORT', 5432) : Reads the DATABASE_PORT environment variable as an integer to set the database port. Defaults to 5432 , the standard PostgreSQL port. database: env('DATABASE_NAME', 'strapi') : Reads DATABASE_NAME to determine which database Strapi should use. Defaults to 'strapi' . Add Environment Variables Setting environment variables ensures Strapi connects to the correct database without hardcoding sensitive values. Using a .env file keeps credentials secure, configurable across environments, and consistent with deployment best practices. # .env DATABASE_HOST=localhost DATABASE_PORT=5432 DATABASE_NAME=vodcms DATABASE_USERNAME=postgres DATABASE_PASSWORD=your_secure_password_here Build & Start the Project Run the build command to generate the admin panel and then start the server. Strapi will automatically create the tables in your PostgreSQL database. npm run build npm run develop Content Models for VOD Define a Video collection with fields like Title, Description, File/URL, Thumbnail, Duration, Tags, Category, and Access Level (Free/Premium). Metadata implements search, filtering, and personalized recommendations, while supporting scalability when managing large video libraries. { 'kind': 'collectionType', 'collectionName': 'videos', 'info': { 'singularName': 'video', 'pluralName': 'videos', 'displayName': 'Video' }, 'options': { 'draftAndPublish': true }, 'attributes': { 'title': { 'type': 'string', 'required': true }, 'description': { 'type': 'text' }, 'videoFile': { 'type': 'media', 'required': true, 'allowedTypes': ['videos'] }, 'thumbnail': { 'type': 'media', 'allowedTypes': ['images'] }, 'category': { 'type': 'relation', 'relation': 'manyToOne', 'target': 'api::category.category', 'inversedBy': 'videos' }, 'tags': { 'type': 'relation', 'relation': 'manyToMany', 'target': 'api::tag.tag', 'mappedBy': 'videos' } } } Explanation: kind: 'collectionType' : Defines this content type as a collection, meaning it can have multiple entries (videos). info: { singularName, pluralName, displayName } : Metadata for Strapi admin panel. pluginOptions: {} : Placeholder for any plugin-specific configurations (empty here). Integrate Cloudinary for Video Uploads and Transcoding When managing a VOD platform, raw video uploads (like MP4s) are too large and inefficient to stream at scale. To deliver smooth playback across devices and network conditions, videos need to be transcoded into adaptive streaming formats (like HLS). Strapi doesn’t handle transcoding on its own, which is why we integrate Cloudinary. Cloudinary automatically optimizes videos on upload, converts them into streaming-ready formats, and gives you a secure playback URL, ensuring seamless delivery. Install the Cloudinary Provider npm install @strapi/provider-upload-cloudinary Configure the Plugin Create or update ./config/plugins.js to tell Strapi how to connect to Cloudinary using environment variables. // path: ./config/plugins.js module.exports = ({ env }) => ({ upload: { config: { provider: 'cloudinary', providerOptions: { cloud_name: env('CLOUDINARY_CLOUD_NAME'), api_key: env('CLOUDINARY_API_KEY'), api_secret: env('CLOUDINARY_API_SECRET'), }, actionOptions: { upload: { // Upload to a specific folder in your Cloudinary account folder: env('CLOUDINARY_FOLDER', 'vod-cms'), // This tells Cloudinary to generate adaptive streaming formats resource_type: 'auto', }, delete: {}, }, }, }, }); Add Cloudinary Credentials Update your .env file with your account-specific Cloudinary keys: # .env CLOUDINARY_CLOUD_NAME=your_cloud_name CLOUDINARY_API_KEY=your_api_key CLOUDINARY_API_SECRET=your_api_secret CLOUDINARY_FOLDER=vod-cms Restart Strapi Now, when you upload a video through the Admin panel, it will be seamlessly sent to Cloudinary, transcoded, and stored. The video field in your Video entry will contain a secure Cloudinary URL. Implement Role-Based Access Control (RBAC) In a VOD platform, not all videos should be accessible to everyone. Free users can watch open content, but premium videos must be restricted to paying subscribers. Strapi’s default role system provides a starting point, but we need to extend it with a custom policy to enforce fine-grained access rules. Configure Roles in Strapi Admin Navigate to Settings → Roles under the Users & Permissions plugin. Select the Authenticated role (applies to logged-in users). Under Video permissions Enable find and findOne (read access). Do not enable create, update, or delete. Save your changes. This sets baseline permissions, but doesn’t yet distinguish between free and premium videos , which is why we need custom logic. Create a Custom Policy Policies in Strapi act as middleware for authorization logic. We’ll create a custom one to restrict premium content. mkdir -p src/policies touch src/policies/is-premium.js // path: ./src/policies/is-premium.js module.exports = (policyContext, config, { strapi }) => { const { user } = policyContext.state; // get the user from the request context const video = policyContext.params; // get the request parameters // 1. If the user is an admin, always allow. if (user && user.role.name === 'Admin') { return true; } // 2. Find the video being requested return strapi.entityService.findOne('api::video.video', video.id, { populate: ['isPremium'] }) .then((video) => { if (!video) { return false; // Video doesn't exist } // 3. If the video is NOT premium, anyone can watch it. if (!video.isPremium) { return true; } // 4. If the video IS premium, check if the user is authenticated and has premium access. // (This assumes you add a `isPremium: Boolean` field to the User collection type) if (user && user.isPremium) { return true; } // 5. If none of the above, block access. return false; }); }; Apply the policy to the video controller Next, extend the default controller for the video API to use our custom policy before serving content. mkdir -p src/api/video/controllers touch src/api/video/controllers/video.js // path: ./src/api/video/controllers/video.js 'use strict'; const { createCoreController } = require('@strapi/strapi').factories; module.exports = createCoreController('api::video.video', ({ strapi }) => ({ async findOne(ctx) { // Check our custom policy first const isAllowed = await strapi.policy('is-premium').handler(ctx); if (!isAllowed) { return ctx.unauthorized('You are not authorized to view this premium content.'); } // If allowed, proceed with the default Strapi behavior const { data, meta } = await super.findOne(ctx); return { data, meta }; } })); Rebuild and Restart Strapi Rebuild the project to apply changes: npm run build npm run develop Media Handling & Transcoding Workflow Raw MP4 uploads are inefficient for large-scale delivery. A production-ready VOD platform needs adaptive streaming formats like HLS or DASH, which adjust quality based on the viewer’s device and bandwidth. To achieve this, combine cloud storage with a transcoding service that converts uploaded MP4s into multiple resolutions, generates adaptive bitrate streams for smooth playback, and outputs secure playback URLs. Strapi stays in sync via webhooks . When a video is transcoded, the external service sends a callback to update Strapi automatically, ensuring your backend always reflects the correct playback status. // src/api/webhooks/controllers/video.js module.exports = { async handle(ctx) { const { type, data } = ctx.request.body; if (type === 'video.processed') { await strapi.db.query('api::video.video').update({ where: { sourceId: data.id }, data: { playbackUrl: data.playbackUrl }, }); } ctx.send({ received: true }); }, }; Explanation : module.exports = ({ env }) => ({ ... }) : Exports a function that reads environment variables and sets plugin configurations for Strapi. upload: { config: { ... } } : Configures the upload plugin, which handles media (images, videos, files) in Strapi. providerOptions: { cloud_name, api_key, api_secret } : Reads environment variables to securely provide Cloudinary credentials.