⚠️ IMPORTANT (Story 6.10.1): TendSocial enforces private-only media delivery. Do NOT configure a public CDN domain or public-access bucket policy. This guide documents private R2 setup only.
Prerequisites
- Cloudflare account with R2 enabled
- R2 bucket already created (e.g.,
tendsocial)
Step 1: Verify R2 Bucket is Private (Required)
- Log in to your Cloudflare Dashboard
- Go to R2 in the left sidebar
- Select your bucket (e.g.,
tendsocial) - Click on the Settings tab
- Verify:
- No public-access policy is enabled
- Managed domain (r2.dev) is DISABLED or not present
- Custom domains section is EMPTY or all domains are disabled
If any public access is enabled, disable it immediately. The deployment verification script will fail if the bucket has public access.
Step 2: Retrieve Private R2 Endpoint
To optimize CDN performance:
- Go to Cache → Cache Rules in your Cloudflare dashboard
- Create a new rule:
- Name: R2 Image Caching
- When incoming requests match:
Hostname equals cdn.tendsocial.com - Then:
- Cache eligibility: Eligible for cache
- Cache TTL: 1 year (or custom)
- Browser TTL: 1 year
- Save and deploy
Step 3: Configure CORS for Presigned URLs
[
{
"AllowedOrigins": [
"https://app.tendsocial.com",
"http://localhost:5173"
],
"AllowedMethods": [
"GET",
"PUT",
"POST",
"DELETE",
"HEAD"
],
"AllowedHeaders": [
"*"
],
"ExposeHeaders": [
"ETag"
],
"MaxAgeSeconds": 3600
}
]Note: Update AllowedOrigins to match your actual frontend domains.
Step 4: Update Environment Variables
Update your backend environment variables:
# Cloudflare R2 Configuration (PRIVATE ONLY)
S3_BUCKET=tendsocial
S3_REGION=auto
S3_ENDPOINT=https://370e3c7ed6e50972fea1a19da4eb21c6.r2.cloudflarestorage.com
AWS_ACCESS_KEY_ID=your-r2-access-key-id
AWS_SECRET_ACCESS_KEY=your-r2-secret-access-key
# Media Proxy Configuration (for private asset delivery)
MEDIA_PROXY_BASE_URL=https://media-proxy.tendsocial.com
MEDIA_PROXY_SECRET=your-media-proxy-secret-key
# Cloudflare API (for deployment verification — optional but recommended)
CLOUDFLARE_API_TOKEN=your-cloudflare-api-token
CLOUDFLARE_ACCOUNT_ID=your-cloudflare-account-id⚠️ Do NOT set: CDN_DOMAIN, CDN_BASE_URL, R2_PUBLIC_BUCKET, R2_DEV_URL, ASSET_BASE_URL, MEDIA_CDN_URL
The server will refuse to start if any public-delivery env vars are present.
For Google Cloud Run:
- Go to your Cloud Run service
- Edit & Deploy New Revision
- Update environment variables (remove any CDN_DOMAIN, add MEDIA_PROXY_* if needed)
- Deploy
Step 5: Verify Deployment
Run the deployment verification script to ensure the R2 bucket is private:
cd apps/backend
npx tsx src/scripts/verify-deployment.tsExpected output:
✅ Deployment verification passed
✓ Configuration gate: OK (no public-CDN env vars)
✓ Live R2 bucket: private (no r2.dev, no custom domains)Step 6: Test Presigned URL Generation
# Generate presigned URL for authenticated in-app rendering
curl -X POST https://api.tendsocial.com/api/assets/presigned-view \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"key": "company-123/images/user-456/test.png"
}'Expected response:
{
"viewUrl": "https://account.r2.cloudflarestorage.com/...?X-Amz-Algorithm=...",
"expiresIn": 900
}The viewUrl is a short-lived presigned URL (15 min TTL) for authenticated in-app rendering only.
Troubleshooting
Deployment Verification Fails
Error: "r2.dev public-access endpoint is not allowed" or "custom domain is active"
Fix:
- Go to R2 bucket settings in Cloudflare Dashboard
- Verify Managed domain (r2.dev) is disabled
- Verify Custom domains section is empty or all domains are disabled
- Re-run
npx tsx src/scripts/verify-deployment.ts
CORS Errors in Browser
- Verify CORS policy is correctly configured in R2 bucket settings
- Ensure your frontend domain is in the
AllowedOriginslist - Check browser console for specific CORS error messages
Presigned URLs Not Working
- Verify R2 credentials (
AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY) are correct - Verify R2 endpoint is private (not
.r2.dev) - Check that the presigned URL has not expired (15-minute TTL)
- Verify the request includes valid JWT token for auth
Cloudflare API Errors During Verification
- Verify
CLOUDFLARE_API_TOKENis correct and not expired - Verify
CLOUDFLARE_ACCOUNT_IDmatches your actual Cloudflare account - Verify
S3_BUCKETorR2_BUCKETmatches the actual R2 bucket name - Check that the API token has R2 read permissions
Security Best Practices
- Keep R2 Bucket Private: No public-access policy, no r2.dev, no custom domains
- Use Presigned URLs for In-App Rendering: TTL is hard-capped at 15 minutes
- Use Media Proxy for External Delivery: Verified-domain proxy handles platform auth and leasing
- Validate File Types: Always validate on the server before upload
- Monitor Usage: Set up alerts for unusual access patterns
- Rate Limiting: Upload endpoints have rate limits (100 req/min per IP)
- Regular Audits: Run deployment verification script regularly to catch public-access misconfigurations
Cost Optimization
- R2 Storage: $0.015/GB/month
- Class A Operations (writes): $4.50 per million requests
- Class B Operations (reads): $0.36 per million requests
- Egress: Charged for direct requests; eliminate via media proxy for platform uploads
Strategy: Route all external platform uploads through the media proxy to centralize egress and authentication.
Deployment Checklist
- [ ] R2 bucket is created and configured
- [ ] R2 credentials are stored securely (Google Secret Manager in production)
- [ ] Managed domain (r2.dev) is disabled
- [ ] Custom domains section is empty or all domains are disabled
- [ ] CORS policy is configured for presigned URLs
- [ ]
MEDIA_PROXY_BASE_URLandMEDIA_PROXY_SECRETare set - [ ]
CDN_DOMAIN/CDN_BASE_URLand other public-delivery vars are NOT set - [ ] Deployment verification script passes:
npx tsx src/scripts/verify-deployment.ts - [ ] Monitor R2 usage in Cloudflare dashboard
- [ ] Set up alerts for storage limits and unusual access patterns