Skip to content

feat: Migrate resume PDF to Cloudflare R2 with signed URL delivery #166

@taearls

Description

@taearls

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:

  1. Stores the PDF in a private R2 bucket
  2. Generates time-limited signed URLs for secure access
  3. Tracks download analytics in KV storage
  4. 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 exceeded
  • 500 Internal Server Error - R2 unavailable

UI Updates

Update src/pages/ResumePage.tsx download button to:

  1. Fetch signed URL from Worker on click
  2. Show loading state during fetch
  3. Redirect browser to signed URL for download
  4. 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.com and 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-resume via Wrangler
  • Create KV namespace RESUME_ANALYTICS for download tracking
  • Upload tyler-earls-resume.pdf to 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 useResumeDownload hook for fetching signed URL
  • Update download button with loading/error states
  • Add ActionButton or 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.toml with custom domain route
  • Remove public/tyler-earls-resume.pdf after 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 Worker
  • src/pages/ResumePage.tsx - Download button update
  • src/hooks/useResumeDownload.ts (new) - Download hook
  • packages/shared-types/src/index.ts - Add resume API types
  • wrangler.toml (root or workers/resume/) - R2 and KV bindings

Priority

🟢 Medium - Improves infrastructure but not blocking

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions