DecipherIt uses Cloudflare R2 for object storage, providing cost-effective and performant storage for uploaded documents and generated audio files.
Overview
Cloudflare R2 is used for:
- Document Uploads - PDF, DOCX, PPTX, XLSX files uploaded by users
- Audio Files - AI-generated podcast-style audio overviews
- Public Access - Files served through custom domain with CDN
Prerequisites
- Cloudflare account with R2 enabled
- R2 bucket created
- Custom domain configured (optional but recommended)
Environment Variables
Frontend Configuration
Add to client/.env.local:
R2_ENDPOINT=https://your-account-id.r2.cloudflarestorage.com
R2_ACCESS_KEY_ID=your_r2_access_key_id
R2_SECRET_ACCESS_KEY=your_r2_secret_access_key
R2_BUCKET_NAME=decipher-files
R2_PUBLIC_URL=https://files.decipherit.xyz
Backend Configuration
Add to backend/.env:
CLOUDFLARE_ACCOUNT_ID=your_cloudflare_account_id
CLOUDFLARE_R2_ACCESS_KEY_ID=your_r2_access_key_id
CLOUDFLARE_R2_SECRET_ACCESS_KEY=your_r2_secret_access_key
Setup Guide
Log in to Cloudflare Dashboard
Navigate to R2 Object Storage
Click Create bucket
Enter bucket name: decipher-files
Choose location (automatic is recommended)
Click Create bucket
In R2 dashboard, go to Manage R2 API Tokens
Click Create API token
Configure permissions:
- Object Read & Write on
decipher-files bucket
Click Create API token
Copy the Access Key ID and Secret Access Key
Add them to your environment variables
In Cloudflare dashboard, scroll to the bottom right
Copy your Account ID
Add it to your environment variables
In R2 bucket settings, go to Settings
Under Public Access, click Allow Access
Configure custom domain:
- Click Connect Domain
- Enter your domain:
files.decipherit.xyz
- Follow DNS configuration steps
Update R2_PUBLIC_URL environment variable with your custom domain
Frontend Integration
R2 Client Setup
The frontend uses AWS SDK S3 client for R2:
import { S3Client } from "@aws-sdk/client-s3";
// Cloudflare R2 configuration
export const r2Client = new S3Client({
region: "auto", // R2 uses "auto" region
endpoint: process.env.R2_ENDPOINT,
credentials: {
accessKeyId: process.env.R2_ACCESS_KEY_ID!,
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!,
},
});
export const R2_BUCKET_NAME = process.env.R2_BUCKET_NAME!;
Supported File Types
Documents that can be uploaded:
export const SUPPORTED_FILE_TYPES = {
'application/pdf': '.pdf',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': '.docx',
'application/vnd.openxmlformats-officedocument.presentationml.presentation': '.pptx',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': '.xlsx',
} as const;
export const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
File Upload Implementation
// Generate unique file path
export function generateR2FilePath(userId: string, originalFilename: string): string {
const timestamp = Date.now();
const randomId = Math.random().toString(36).substring(2, 8);
const cleanFilename = originalFilename.replace(/[^a-zA-Z0-9.-]/g, '_');
return `uploads/${userId}/${timestamp}-${randomId}-${cleanFilename}`;
}
// Get public URL for file
export function getR2PublicUrl(filePath: string): string {
return `${process.env.R2_PUBLIC_URL}/${filePath}`;
}
Upload API Route
File upload handled by Next.js API route:
client/app/api/upload/route.ts
import { PutObjectCommand } from "@aws-sdk/client-s3";
import { r2Client, R2_BUCKET_NAME, generateR2FilePath } from "@/lib/r2";
export async function POST(request: Request) {
const formData = await request.formData();
const file = formData.get("file") as File;
// Generate file path
const filePath = generateR2FilePath(userId, file.name);
// Upload to R2
await r2Client.send(
new PutObjectCommand({
Bucket: R2_BUCKET_NAME,
Key: filePath,
Body: Buffer.from(await file.arrayBuffer()),
ContentType: file.type,
})
);
// Return public URL
return Response.json({
url: getR2PublicUrl(filePath),
filename: file.name
});
}
Backend Integration
R2 Service Setup
The backend uses boto3 for R2 access:
backend/services/r2_service.py
import os
import boto3
from botocore.exceptions import ClientError
import uuid
class R2Service:
"""Service for uploading files to Cloudflare R2 storage."""
def __init__(self):
self.account_id = os.environ.get("CLOUDFLARE_ACCOUNT_ID")
self.access_key_id = os.environ.get("CLOUDFLARE_R2_ACCESS_KEY_ID")
self.secret_access_key = os.environ.get("CLOUDFLARE_R2_SECRET_ACCESS_KEY")
self.bucket_name = "decipher-files"
# Configure R2 endpoint
self.endpoint_url = f"https://{self.account_id}.r2.cloudflarestorage.com"
# Initialize S3 client for R2
self.s3_client = boto3.client(
"s3",
endpoint_url=self.endpoint_url,
aws_access_key_id=self.access_key_id,
aws_secret_access_key=self.secret_access_key,
region_name="auto" # R2 uses 'auto' as region
)
Audio File Upload
Audio overviews are uploaded to R2:
backend/services/r2_service.py
async def upload_audio_file(
self,
audio_content: bytes,
notebook_id: str,
file_extension: str = "mp3"
) -> str:
"""Upload audio file to R2 bucket."""
# Generate file key
random_id = str(uuid.uuid4())[:8]
file_key = f"audios/{notebook_id}_audio_overview_{random_id}.{file_extension}"
# Upload file to R2
self.s3_client.put_object(
Bucket=self.bucket_name,
Key=file_key,
Body=audio_content,
ContentType=f"audio/{file_extension}",
ACL="public-read" # Make file publicly accessible
)
# Generate public URL
public_url = f"https://files.decipherit.xyz/{file_key}"
return public_url
Audio Overview Workflow
Complete workflow for audio generation and storage:
backend/services/audio_overview_service.py
from .tts_service import tts_service
from .r2_service import r2_service
from .notebook_repository import notebook_repository
class AudioOverviewService:
async def generate_complete_audio_overview(self, notebook_id: str):
# Step 1: Generate transcript using AI agent
transcript = await run_audio_overview_agent(notebook_id)
# Step 2: Generate TTS audio from transcript
audio_content = await tts_service.generate_audio_from_transcript(
transcript, notebook_id
)
# Step 3: Upload audio file to R2
audio_url = await r2_service.upload_audio_file(
audio_content, notebook_id
)
# Step 4: Update database with audio URL
await notebook_repository.update_audio_overview_url(notebook_id, audio_url)
return {
"transcript": transcript,
"audio_url": audio_url,
"status": "completed"
}
File Organization
Folder Structure
Files are organized by type and user:
decipher-files/
├── uploads/
│ └── {userId}/
│ └── {timestamp}-{randomId}-{filename}
└── audios/
└── {notebookId}_audio_overview_{randomId}.mp3
Naming Conventions
// Format: uploads/{userId}/{timestamp}-{randomId}-{cleanFilename}
const filePath = generateR2FilePath("user123", "Research Paper.pdf");
// Result: "uploads/user123/1234567890-abc123-Research_Paper.pdf"
Security Best Practices
Never commit credentials to version controlAlways use environment variables for R2 credentials.
Access Control
- Use scoped API tokens with minimum required permissions
- Separate tokens for frontend (upload) and backend (upload + delete)
- Rotate tokens regularly
File Validation
Validate files before upload:
import { SUPPORTED_FILE_TYPES, MAX_FILE_SIZE } from "@/lib/r2";
// Validate file type
if (!SUPPORTED_FILE_TYPES[file.type]) {
throw new Error("Unsupported file type");
}
// Validate file size
if (file.size > MAX_FILE_SIZE) {
throw new Error("File too large (max 10MB)");
}
Public Access
Files are made publicly accessible for serving:
backend/services/r2_service.py
self.s3_client.put_object(
Bucket=self.bucket_name,
Key=file_key,
Body=audio_content,
ContentType=f"audio/{file_extension}",
ACL="public-read" # Public read access
)
CDN Caching: Public files are automatically cached by Cloudflare’s CDN for fast global delivery.
Cost Optimization
R2 Pricing
Cloudflare R2 offers:
- No egress fees (unlike AWS S3)
- Low storage costs: $0.015/GB per month
- Free tier: 10 GB storage, 1 million Class A operations/month
Best Practices
- Enable custom domain for better caching
- Set appropriate cache headers for static content
- Delete old files periodically (implement lifecycle policies)
- Use compression for uploaded documents when possible
Troubleshooting
Upload Failures
If uploads fail:
- Verify R2 credentials are correct
- Check bucket name matches configuration
- Ensure API token has write permissions
- Check file size is under limit (10MB)
Access Denied Errors
botocore.exceptions.ClientError: An error occurred (AccessDenied)
Fix by:
- Verifying API token permissions
- Checking bucket policy allows public reads
- Ensuring account ID is correct
CORS Issues
If frontend uploads fail with CORS errors:
- In R2 bucket settings, configure CORS:
[
{
"AllowedOrigins": ["https://decipherit.xyz"],
"AllowedMethods": ["GET", "PUT", "POST"],
"AllowedHeaders": ["*"]
}
]
Monitoring
Monitor R2 usage:
- Cloudflare dashboard shows storage usage
- Track API operations count
- Monitor egress (should be minimal with CDN)
Next Steps