Building a Presigned Upload Flow for QDN Dashboard: Lambda, S3, and CloudFront Integration
What Was Done
We extended the QuickDumpNow dashboard with a new on-the-go photo capture workflow by adding presigned URL generation to the Lambda backend and wiring upload buttons into the job drawer UI. The implementation reuses our existing S3 presigned GET infrastructure (for displaying photos) and extends it with presigned POST URLs (for uploads), all served through a new /upload-url API Gateway endpoint.
The Problem We Solved
Field workers needed a faster way to capture and upload photos directly from the dashboard without leaving the job context. The previous flow required navigating to a separate upload page. By adding presigned URL generation to the Lambda function, we could offer instant, time-limited upload credentials issued per-job, keeping the user in the job drawer while photos upload asynchronously.
Architecture and Technical Details
Lambda Function Enhancement
We modified /Users/cb/Documents/repos/sites/dashboard.quickdumpnow.com/lambda/lambda_function.py to add a new route handler for presigned URL generation. The function already contained a list_photos method issuing presigned GET URLs; we extended this pattern with a new method generating presigned POST URLs:
def presign_upload(self, job_id):
"""Generate presigned POST URL for S3 uploads"""
bucket = os.environ['UPLOADS_BUCKET']
key = f"jobs/{job_id}/{uuid4()}.jpg"
response = self.s3.generate_presigned_post(
Bucket=bucket,
Key=key,
ExpiresIn=900, # 15 minutes
Conditions=[
['content-length-range', 0, 52428800] # 50MB max
]
)
return response
The presigned POST response includes the endpoint URL, form fields, and signature—everything a frontend needs to upload directly to S3 without touching our Lambda again. We set expiry to 15 minutes to balance usability (field work can be slow) with security (limiting credential window).
API Gateway Route Addition
We added two new routes to the API Gateway integration for the dashboard Lambda:
GET /upload-url– Issues a presigned POST URL for a given job IDPOST /upload-url– Logs upload metadata (job ID, timestamp, file count) to DynamoDB for audit trails
The GET endpoint returns a JSON response matching AWS S3's presigned POST format:
{
"url": "https://qdn-uploads.s3.amazonaws.com/",
"fields": {
"key": "jobs/j-12345/abc-def-ghi.jpg",
"policy": "eyJleHBpcmF0aW9uIjogIjIwMjQtMDEtMTVUMDg6NDU6MDBaIiw...",
"x-amz-signature": "abc123...",
"x-amz-date": "20240115T083000Z"
}
}
S3 Bucket Configuration
The uploads go to qdn-uploads (private bucket, no public ACL). We verified the Lambda's IAM role (qdn-lambda-role) had the required permissions:
s3:GetObjectonarn:aws:s3:::qdn-uploads/*– for reading uploaded filess3:PutObjectonarn:aws:s3:::qdn-uploads/*– for presigned POST generation
We confirmed CORS was not needed because presigned POST is generated server-side; the browser receives the complete form structure, not a cross-origin request to S3.
Dashboard UI Changes
We modified /Users/cb/Documents/repos/sites/dashboard.quickdumpnow.com/index.html to add a capture button in the job drawer. The button triggers a JavaScript function that:
- Calls
GET /upload-url?job_id=j-12345 - Receives presigned POST fields
- Builds an HTML form and submits it with the file input
- On success, refreshes the photo thumbnail strip via
list_photos
This keeps the UX modal—no page navigation—while leveraging browser's native multipart form handling.
Key Infrastructure Decisions
Why Presigned URLs Instead of Upload-Through-Lambda
We could have had the browser POST to /upload in our Lambda, which then forwarded to S3. We rejected this because:
- Lambda timeout risk: Large files or slow connections could exceed the 30-second timeout
- Cost: Every byte of the upload would consume Lambda compute time and network egress
- Concurrency: Multiple simultaneous uploads would consume Lambda execution slots
Presigned URLs are stateless and serverless—S3 handles the PUT, we handle only the credentials audit.
Why POST URLs and Not Direct IAM Credentials
S3 presigned POST URLs are safer than embedding AWS credentials in the frontend because they:
- Expire automatically (15 minutes)
- Are single-use per file (key-specific)
- Don't expose the AWS account ID
- Can include Conditions (file size limits, content-type restrictions)
CloudFront and Photo Display
The dashboard itself is served through CloudFront (distribution ID E2ABC1234XYZ) and cached aggressively. Photos uploaded to qdn-uploads are accessed via the Lambda's list_photos method, which generates presigned GET URLs with 1-hour expiry. This decouples CloudFront caching from dynamic photo access.
Deployment and Testing
We deployed the updated Lambda function via AWS CLI (no new environment variables required—it reuses existing UPLOADS_BUCKET). We then smoke-tested the new endpoint:
curl "https://api.quickdumpnow.com/upload-url?job_id=j-test" \
-H "Authorization: Bearer $AUTH_TOKEN"
Confirmed the response included valid presigned POST fields. We then staged updated HTML to the dashboard bucket staging directory and verified the capture button appeared and generated valid forms.
What's Next
With presigned uploads in place, the remaining work includes:
- Photo processing pipeline: Add Lambda trigger on S3 uploads to resize/optimize images for thumbnail display
- iOS integration: Wire the
/upload-urlendpoint into the iOS GPS Shortcut (separate project) so mobile workers can upload without opening the web dashboard - Stripe booking flow: Integrate payment processing with job creation so customers pay upfront and field workers see only confirmed jobs
The presigned URL infrastructure is now ready to serve all three workstreams: web dashboard, mobile shortcuts, and payment-gated job booking.