Building a Presigned Upload Flow for QDN's On-the-Go Capture: Lambda, S3, and CloudFront Integration
What Was Done
We implemented a client-side photo upload capability for QuickDumpNow's dashboard, enabling users to capture and upload photos directly from the job drawer without leaving the dashboard interface. This required:
- Adding a new
/upload-urlLambda endpoint that generates presigned S3 URLs for PUT operations - Extending the existing
qdn-upload-presignLambda function to support both quote generation and direct uploads - Wiring UI components in the dashboard job drawer to trigger uploads and display thumbnails
- Securing the S3 bucket (
qdn-uploads) with appropriate CloudFront-only access patterns
Technical Details: The Lambda Architecture
File: /Users/cb/Documents/repos/sites/dashboard.quickdumpnow.com/lambda/lambda_function.py
The Lambda function handles multiple endpoints through a routing pattern based on the request path. We extended the existing presign logic to support upload URLs alongside the existing pricing and booking flows:
def lambda_handler(event, context):
path = event.get('path', '')
if path == '/upload-url':
return handle_upload_url(event)
elif path == '/book':
return handle_booking(event)
elif path == '/quote':
return handle_quote(event)
The handle_upload_url function generates a presigned PUT URL for the qdn-uploads S3 bucket:
def handle_upload_url(event):
body = json.loads(event.get('body', '{}'))
job_id = body.get('job_id')
file_name = body.get('file_name')
s3_client = boto3.client('s3')
presigned_url = s3_client.generate_presigned_url(
'put_object',
Params={
'Bucket': 'qdn-uploads',
'Key': f'{job_id}/{file_name}'
},
ExpiresIn=3600
)
return {
'statusCode': 200,
'body': json.dumps({'upload_url': presigned_url})
}
Why presigned URLs? This pattern allows the frontend to upload directly to S3 without the Lambda function becoming a bottleneck. The Lambda only generates credentials valid for one hour, limiting exposure if a URL is leaked. The S3 bucket itself remains private (no public ACLs), accessible only through CloudFront for reads.
Infrastructure: S3, Bucket Policies, and CloudFront
S3 Bucket Configuration: qdn-uploads
The bucket is configured with:
- Block Public Access: All public ACL and policy blocks enabled
- Bucket Policy: Restricts access to the Lambda execution role (
qdn-lambda-role) and CloudFront Origin Access Identity - Versioning: Enabled for audit trail
The bucket policy follows this pattern (without exposing actual resource ARNs):
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::ACCOUNT:role/qdn-lambda-role"
},
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::qdn-uploads",
"arn:aws:s3:::qdn-uploads/*"
]
},
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:cloudfront:us-east-1::distribution/CLOUDFRONT_OAI"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::qdn-uploads/*"
}
]
}
CloudFront Configuration: The dashboard.quickdumpnow.com distribution uses an Origin Access Identity (OAI) to ensure all reads go through CloudFront, enabling caching and DDoS protection. The origin is the qdn-uploads bucket with no direct public access.
Dashboard UI Integration
File: /Users/cb/Documents/repos/sites/dashboard.quickdumpnow.com/index.html
The job drawer includes a capture section with two components:
- Capture Button: Triggers the device camera/file picker, collects the image, and calls
/upload-urlto get a presigned URL - Thumbnail Strip: Displays recently uploaded photos for the current job, with presigned GET URLs generated by the Lambda's
list_photosendpoint
The client-side flow:
async function uploadPhoto(jobId, file) {
// Step 1: Get presigned PUT URL from Lambda
const presignResponse = await fetch('/upload-url', {
method: 'POST',
body: JSON.stringify({
job_id: jobId,
file_name: file.name
})
});
const { upload_url } = await presignResponse.json();
// Step 2: Upload directly to S3
const uploadResponse = await fetch(upload_url, {
method: 'PUT',
body: file,
headers: { 'Content-Type': file.type }
});
// Step 3: Refresh thumbnail strip
if (uploadResponse.ok) {
refreshPhotoList(jobId);
}
}
API Gateway and Routing
Resource: API Gateway routes for dashboard.quickdumpnow.com
We added new routes to the existing API Gateway:
POST /upload-url→ Lambda integrationGET /list-photos→ Lambda integration (generates presigned GET URLs for existing photos)
Each route uses AWS_IAM authorization, requiring CloudFront to sign requests via a resource-based policy.
Key Decisions and Rationale
Why Presigned URLs Instead of Direct Uploads Through Lambda? Direct uploads through Lambda would require the function to buffer the entire file in memory and perform the S3 PutObject operation, limiting concurrency and increasing latency. Presigned URLs shift the upload workload directly to S3, allowing the frontend to retry on failure without Lambda involvement.
Why One-Hour Expiration? A one-hour window balances security (limiting leaked URL reuse) with UX (users won't encounter expired URLs during normal job site visits). If a URL expires, the client simply requests a new one.
Why CloudFront for the Upload Bucket? Although the bucket stores job site photos, we route all reads through CloudFront to enable:
- Geographic distribution and edge caching
- DDoS protection