```html

Building Presigned Upload URLs for QuickDumpNow's Dashboard: AWS Lambda + S3 Integration

What Was Done

We implemented a presigned URL generation system for the QuickDumpNow dashboard to enable direct client-side uploads to S3, eliminating the need for uploads to transit through our Lambda functions. This reduces latency, decreases Lambda execution time, and improves the user experience for on-the-go photo capture during jobs.

The implementation added a new /upload-url endpoint to the Lambda function, wired job drawer capture buttons to request presigned URLs, and modified the photo listing logic to generate presigned GET URLs for displaying uploaded images through the CloudFront-private dashboard.

Technical Details

Lambda Function Architecture

The core changes were made in /Users/cb/Documents/repos/sites/dashboard.quickdumpnow.com/lambda/lambda_function.py. We extended the existing API Gateway routing logic to handle a new route:

POST /upload-url → generatePresignedUploadUrl()
GET /list-photos → listPhotos() [modified to use presigned GET URLs]

The generatePresignedUploadUrl() function accepts a job ID and file metadata, then returns a presigned POST URL valid for 15 minutes. This matches the existing pattern used by the qdn-upload-presign Lambda (which we inspected and reused the signature strategy from).

Key function signature:

def generatePresignedUploadUrl(bucket, job_id, filename, content_type):
    """
    Generate a presigned POST URL for S3 uploads.
    Returns tuple of (presigned_url, fields_dict) for multipart form POST.
    """

The function leverages boto3's generate_presigned_post() method rather than generate_presigned_url() because browser-based uploads require POST requests with form fields, not GET URLs.

S3 Bucket Configuration

All uploads target the qdn-uploads S3 bucket (verified public-by-default, with bucket policy allowing CloudFront GET access). The bucket is configured with:

  • Bucket Policy: Allows s3:GetObject from CloudFront origin access identity (for photo display)
  • CORS Configuration: Added permissive CORS rules for browser-based PUT/POST requests from dashboard.quickdumpnow.com
  • IAM Role Permissions: The Lambda execution role (qdn-lambda-role) has inline policies granting s3:PutObject, s3:GetObject, and s3:GetObjectVersion on the uploads bucket

Dashboard UI Changes

The job drawer in /Users/cb/Documents/repos/sites/dashboard.quickdumpnow.com/index.html was modified to:

  • Add a "Capture Photo" button that calls requestUploadUrl(jobId)
  • Display a thumbnail strip below the job details showing previously uploaded photos
  • Use the thumbnail URIs from /list-photos endpoint, which now returns presigned GET URLs (valid 1 hour)

The capture flow:

  1. User clicks "Capture" in the job drawer
  2. Browser calls POST /upload-url with job ID and filename
  3. Lambda returns presigned POST fields (action URL + form fields)
  4. JavaScript submits file directly to S3 using the presigned form
  5. On success, /list-photos is called to refresh the thumbnail strip with updated presigned URLs

Infrastructure & Deployment

API Gateway Configuration

Added routes to the existing API Gateway instance (serving dashboard.quickdumpnow.com):

POST /upload-url
GET /list-photos

Both routes integrate with the Lambda function, passing query parameters and request body through the standard CloudFormation template or AWS console.

CloudFront Distribution

The dashboard is served via CloudFront distribution (ID verified via AWS console listing) with:

  • Origin: Lambda function URL (API Gateway endpoint)
  • Behavior: Cache disabled for /upload-url and /list-photos (dynamic responses)
  • Security: Origin Access Identity restricts S3 reads to CloudFront only; direct bucket access is blocked

Deployment Process

Lambda updates were deployed via the standard workflow:

cd /Users/cb/Documents/repos/sites/dashboard.quickdumpnow.com/lambda
# Syntax validation
python -m py_compile lambda_function.py

# Package and deploy
aws lambda update-function-code \
  --function-name qdn-dashboard-api \
  --zip-file fileb://function.zip

# Verify deployment
aws lambda invoke --function-name qdn-dashboard-api response.json

Dashboard HTML files were deployed to the staging directory on the dashboard.quickdumpnow.com bucket, then promoted to production after verification:

aws s3 cp index.html s3://dashboard.quickdumpnow.com/staging/
# Verify staging URL
curl https://dashboard.quickdumpnow.com/staging/index.html

# Promote to production
aws s3 cp s3://dashboard.quickdumpnow.com/staging/index.html \
  s3://dashboard.quickdumpnow.com/index.html

Key Decisions & Rationale

  • Presigned POST over PUT: CloudFormation and browser JavaScript libraries have native support for presigned POST forms. PUT requests require custom header signing, which is more fragile in browsers.
  • 15-minute upload URL lifetime: Short TTL limits damage from URL leakage while remaining long enough for real users with poor connectivity. Presigned URLs are single-use by design (S3 validates the signature on each request).
  • CloudFront-only S3 access: The dashboard.quickdumpnow.com bucket is private; users cannot access photos via direct S3 URLs. We generate presigned GET URLs (1-hour TTL) for display, ensuring access control remains with our Lambda logic.
  • Separate upload bucket: We upload to qdn-uploads (not the dashboard bucket) to keep concerns separated and simplify bucket policies.

What's Next

With presigned uploads in place, the next phase includes:

  • Implementing the iOS GPS Shortcut to capture location metadata alongside photos
  • Extending the Stripe booking flow with payment method selection (Zelle picker already staged in qdn-book-index-v2.html)
  • Monitoring CloudWatch logs for upload failures and implementing retry logic on the client side

All three workstreams are tracked in the kanban board (cards: t-dc0e6300, t-70