Private Assets

JWT-signed URLs for paid content and premium video access.

How It Works

text
1. Mark asset as private (is_private: true)
2. When user requests access, your backend generates a signed URL
3. Signed URL contains: asset_id, expiry time, optional IP lock
4. MediaKit validates the signature before serving content
5. Expired or tampered URLs return 403 Forbidden

Mark Asset as Private

bash
curl -X PATCH /api/assets/1 \
  -H "Authorization: Bearer TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"is_private": true}'

Generate Signed URL

bash
curl -X POST /api/mediakit/signed-url \
  -H "Authorization: Bearer TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "asset_id": 1,
    "expires_in": 3600,
    "ip": "203.0.113.50"
  }'
Response
{
  "data": {
    "url": "https://mediakitapi.gritcms.com/api/mediakit/private/1?token=eyJhbGci...",
    "expires_at": "2026-03-29T13:00:00Z"
  }
}

Parameters

FieldTypeDescription
asset_idnumberAsset to access
expires_innumberSeconds until expiry (default: 3600)
ipstringOptional IP lock (only this IP can use the URL)

Use Case: Paid Courses

api/lesson/[id].ts
// Your backend checks if user has purchased the course
const hasPurchased = await checkPurchase(userId, courseId);
if (!hasPurchased) return res.status(403).json({ error: 'Not purchased' });

// Generate a time-limited signed URL
const { data } = await fetch('/api/mediakit/signed-url', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${serviceToken}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    asset_id: lesson.videoAssetId,
    expires_in: 7200,  // 2 hours
    ip: req.headers['x-forwarded-for'],
  }),
}).then(r => r.json());

// Return signed URL to client
return res.json({ video_url: data.url });

Configuration

Set the signing secret in your .env:

env
SIGNED_URL_SECRET=your-32-character-hex-secret

Generate with: openssl rand -hex 32