Primary video hosting for protected content.
- Create Cloudflare account
- Enable Stream
- Create API token with Stream permissions
- Add to config:
env:
cloudflare:
accountId: "..."
apiToken: "..."
signingKey: "..." # Optional: for self-signed tokens
customerSubdomain: "customer-xxx.cloudflarestream.com"Use Cloudflare Dashboard or API.
Video ID after upload: 32 character hex string (e.g., abc123def456789012345678901234ab).
In markdown lesson file, use <vid:> tag:
# Video Lesson
<vid:abc123def456789012345678901234ab>
Text content below video.Also accepts full URLs:
<vid:https://customer-xxx.cloudflarestream.com/abc123.../manifest/video.m3u8>This gets parsed to {{CLOUDFLARE_STREAM:videoId}} for frontend rendering.
Two approaches for generating signed playback URLs:
- Self-signing (with
signingKey): Fast, unlimited tokens - Token API (without
signingKey): Uses Cloudflare API, limited to 1000/day
Flow:
- Client requests:
POST /api/stream/sign-urlwith{ videoId, courseId } - Server verifies user has purchased course
- Server generates signed URL (1 hour expiry default)
- Client plays video with signed URL
POST /api/stream/sign-url - Generate signed playback URL
GET /api/stream/metadata/:id - Get video metadata
GET /api/stream/analytics/:id - Get video analytics
GET /api/stream/validate/:id - Validate video ID exists
With customer subdomain:
https://customer-xxx.cloudflarestream.com/{videoId}/manifest/video.m3u8?token=...
Without (default):
https://videodelivery.net/{videoId}/manifest/video.m3u8?token=...
For non-Cloudflare videos, store files in course directory:
courses/1/
├── 1.md
├── 2.md
└── assets/
└── intro.mp4
Embed in lesson:
<vid:assets/intro.mp4>Or use video file directly as lesson:
courses/1/
├── 1.md
├── 2.mp4 # Video-only lesson
└── 3.md
Supported formats: .mp4, .webm, .mov, .avi
For database-stored video URLs, there's a separate signed URL system:
POST /api/video/access/:lessonId
{
"userId": 123,
"lessonId": 456,
"courseId": 789,
"timestamp": 1234567890
}- Expires in 5 minutes
- Requires
video.signingKeyin config - Access logged to
video_access_logtable
GET /api/video/verify/:token - Verify token (for CDN)
GET /api/video/analytics/:courseId - User's video view history
POST /api/video/report-piracy - Report piracy
- Telegram
initDataheaders are accepted only forenv.telegram.initDataTtlseconds (default 300s) and automatically rejected if reused. - Videos should have
requireSignedURLs: truein Cloudflare Stream settings - All video access is logged with user ID, IP, user agent
- Signed URLs expire (1 hour for Stream, 5 min for legacy)
- Stream signed URLs now require the active
courseIdso the backend can verify the authenticated user purchased that course before returning a token.
Frontend uses CloudflareStreamPlayer component which:
- Detects
{{CLOUDFLARE_STREAM:videoId}}markers - Requests signed URL from server
- Supplies the current
courseIdso the backend can enforce purchase checks - Uses HLS.js for playback
- Shows loading/error states