Cloud Storage Setup (R2 / S3-compatible)
DocDoe serves generated student audio and video through owner-authenticated API routes. Keep any backing object store private; public bucket URLs are not an acceptable delivery mechanism for student-generated artifacts.
When to switch
| Env | STORAGE_PROVIDER | Notes |
|---|---|---|
| Local dev | local (default) |
Files served from /uploads, /generated/audio, /generated/videos |
| Production (single-instance) | r2 or s3 |
Required for stable public video URLs |
| Production (multi-instance) | r2 or s3 |
Required β local volumes don't survive redeploys |
Cloudflare R2 (recommended)
R2 is S3-compatible, zero egress fees, free tier up to 10GB.
1. Create bucket
- Cloudflare Dashboard β R2 β Create bucket
- Bucket name:
docdoe-media - Region: auto
2. Enable public access
Option A β R2.dev subdomain (fastest):
- Bucket β Settings β "Public access" β Allow access via R2.dev subdomain
- Public URL prefix:
https://pub-<hash>.r2.dev
Option B β Custom domain (recommended for production):
- Bucket β Settings β "Public access" β Connect custom domain
- Domain:
media.docdoe.ai - Cloudflare auto-creates CNAME
3. Create API token
- Cloudflare Dashboard β R2 β "Manage R2 API Tokens"
- Create token β "Object Read & Write" permission
- Apply to bucket:
docdoe-media - Copy Access Key ID + Secret Access Key (shown once)
4. Set env vars
STORAGE_PROVIDER=r2
STORAGE_BUCKET=docdoe-media
STORAGE_ENDPOINT_URL=https://<your-account-id>.r2.cloudflarestorage.com
STORAGE_ACCESS_KEY_ID=<R2 access key>
STORAGE_SECRET_ACCESS_KEY=<R2 secret>
STORAGE_PUBLIC_BASE_URL=https://media.docdoe.ai # or https://pub-<hash>.r2.dev
STORAGE_REGION not needed for R2.
5. CORS on bucket
R2 β Bucket β Settings β CORS Policy. Add:
[
{
"AllowedOrigins": ["https://docdoe.ai", "https://www.docdoe.ai"],
"AllowedMethods": ["GET", "HEAD"],
"AllowedHeaders": ["*"],
"MaxAgeSeconds": 3600
}
]
For dev/preview, add Vercel preview origins or https://*.vercel.app.
AWS S3 (alternative)
Same env vars, set STORAGE_PROVIDER=s3 and STORAGE_REGION=us-east-1 (or region of bucket). Endpoint URL not needed β boto3 derives it. Public URLs use https://<bucket>.s3.<region>.amazonaws.com/<key> unless STORAGE_PUBLIC_BASE_URL overrides.
S3 bucket policy (public read)
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicReadGetObject",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::docdoe-media/*"
}
]
}
Plus enable "Block all public access" β uncheck for this bucket.
Verification
After deploy:
# Backend health
curl -s https://api.docdoe.ai/health
# Generate a small test video via /video-generator
# Then list video jobs:
curl -s -H "Authorization: Bearer <jwt>" https://api.docdoe.ai/video/render-jobs
# Final video URL should look like:
# https://media.docdoe.ai/generated-videos/<job-id>/output.mp4
# NOT like:
# http://localhost:8000/generated/videos/<job-id>/output.mp4
If you see localhost URLs in public_url field, STORAGE_PROVIDER is still local. Fix env and redeploy.
Object key layout
| Key prefix | Content |
|---|---|
generated-audio/<job-id>/scene-XXX.wav |
TTS per-scene audio files |
generated-videos/<job-id>/output.mp4 |
Final rendered MP4 |
uploads/<user-id>/<doc-id>.<ext> |
Original student uploads |
Local provider rewrites keys into /generated/audio/..., /generated/videos/..., /uploads/... paths served by FastAPI StaticFiles. R2/S3 provider serves directly from STORAGE_PUBLIC_BASE_URL.
Failure modes
| Symptom | Cause | Fix |
|---|---|---|
| Video plays in dev, 404 in production | STORAGE_PROVIDER=local in prod |
Switch to r2/s3 |
boto3 is not installed error |
Image built without pip install boto3 |
Rebuild Dockerfile.backend (boto3 in requirements.txt) |
| Cloud upload fails | Missing access key, secret, or bucket var | Check /health/deep JSON for storage status |
| Video plays but no audio | Render container missing ffmpeg | Use Dockerfile.backend (includes ffmpeg) |