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 ID
  • POST /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:GetObject on arn:aws:s3:::qdn-uploads/* – for reading uploaded files
  • s3:PutObject on arn: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:

  1. Calls GET /upload-url?job_id=j-12345
  2. Receives presigned POST fields
  3. Builds an HTML form and submits it with the file input
  4. 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-url endpoint 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.