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:GetObjectfrom 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 grantings3:PutObject,s3:GetObject, ands3:GetObjectVersionon 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-photosendpoint, which now returns presigned GET URLs (valid 1 hour)
The capture flow:
- User clicks "Capture" in the job drawer
- Browser calls
POST /upload-urlwith job ID and filename - Lambda returns presigned POST fields (action URL + form fields)
- JavaScript submits file directly to S3 using the presigned form
- On success,
/list-photosis 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-urland/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.combucket 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