-
Notifications
You must be signed in to change notification settings - Fork 0
feat: Migrate resume PDF to Cloudflare R2 with signed URL delivery #166
Copy link
Copy link
Open
Labels
enhancementNew feature or requestNew feature or request
Description
Problem
The resume PDF is currently served as a static file from public/tyler-earls-resume.pdf. This approach has limitations:
- No analytics - Cannot track download counts or usage patterns
- No access control - Anyone with the URL can access indefinitely
- Redeployment required - Updating the resume requires a full site redeploy
- No rate limiting - Vulnerable to abuse or scraping
Proposed Solution
Migrate the resume PDF to Cloudflare R2 Object Storage with a dedicated Resume Worker that:
- Stores the PDF in a private R2 bucket
- Generates time-limited signed URLs for secure access
- Tracks download analytics in KV storage
- Provides rate limiting to prevent abuse
Architecture
┌─────────────────┐ ┌──────────────────────┐ ┌─────────────┐
│ React App │────▶│ Resume Worker │────▶│ R2 Bucket │
│ ResumePage.tsx │ │ api.tylerearls.com │ │ (private) │
└─────────────────┘ │ /api/resume │ └─────────────┘
│ │
│ - Signed URL gen │ ┌─────────────┐
│ - Analytics (KV) │────▶│ KV Store │
│ - Rate limiting │ │ (analytics) │
└──────────────────────┘ └─────────────┘
API Design
Endpoint: GET https://api.tylerearls.com/api/resume
Response (200 OK):
{
"url": "https://[bucket].r2.cloudflarestorage.com/tyler-earls-resume.pdf?X-Amz-Signature=...",
"expiresIn": 300,
"filename": "Tyler Earls - Resume.pdf",
"contentType": "application/pdf",
"size": 116736
}Headers:
Cache-Control: no-store(signed URLs shouldn't be cached)X-Download-Count: 1234(optional analytics header)
Error Responses:
429 Too Many Requests- Rate limit exceeded500 Internal Server Error- R2 unavailable
UI Updates
Update src/pages/ResumePage.tsx download button to:
- Fetch signed URL from Worker on click
- Show loading state during fetch
- Redirect browser to signed URL for download
- Handle errors gracefully with user feedback
Acceptance Criteria
- R2 bucket created and configured with private access
- Resume Worker deployed at
api.tylerearls.com/api/resume - Signed URLs expire after 5 minutes (configurable)
- Download analytics stored in KV (count, last downloaded timestamp)
- Rate limiting prevents >10 requests/minute per IP
- CORS configured for
tylerearls.comand localhost origins - UI shows loading state while fetching signed URL
- UI handles errors with user-friendly message
- Integration tests cover happy path and error states
- Worker tests cover URL signing, analytics, rate limiting
Implementation Tasks
Phase 1: Infrastructure Setup
- Create R2 bucket
portfolio-resumevia Wrangler - Create KV namespace
RESUME_ANALYTICSfor download tracking - Upload
tyler-earls-resume.pdfto R2 bucket - Create
workers/resume/directory with Worker boilerplate
Phase 2: Worker Development
- Implement signed URL generation using R2's
createSignedUrl() - Implement download analytics tracking in KV
- Add rate limiting (reuse pattern from feature-flags Worker)
- Configure CORS headers
- Add environment-specific configuration (dev/staging/prod)
- Write Worker unit tests
Phase 3: UI Updates
- Create
useResumeDownloadhook for fetching signed URL - Update download button with loading/error states
- Add
ActionButtonor similar for async click handling - Handle network errors with retry option
- Write component tests
Phase 4: Deployment & Cleanup
- Deploy Worker to staging, verify functionality
- Deploy Worker to production
- Update
wrangler.tomlwith custom domain route - Remove
public/tyler-earls-resume.pdfafter migration verified - Update ROADMAP.md
Technical Considerations
Security
- Signed URLs: 5-minute expiration prevents URL sharing/scraping
- Private bucket: R2 bucket not publicly accessible
- Rate limiting: 10 req/min per IP prevents abuse
- CORS: Strict origin allowlist
Performance
- Edge delivery: R2 serves from Cloudflare's global network
- No caching of signed URLs: Each request gets fresh URL (security over performance)
- Async download: UI doesn't block while fetching URL
Reliability
- Error handling: Worker returns appropriate error codes
- Fallback: Consider static fallback if Worker unavailable (optional)
- Monitoring: Enable Workers observability for debugging
Files to Modify
workers/resume/(new) - Resume Workersrc/pages/ResumePage.tsx- Download button updatesrc/hooks/useResumeDownload.ts(new) - Download hookpackages/shared-types/src/index.ts- Add resume API typeswrangler.toml(root or workers/resume/) - R2 and KV bindings
Priority
🟢 Medium - Improves infrastructure but not blocking
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
enhancementNew feature or requestNew feature or request