```html

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-url Lambda endpoint that generates presigned S3 URLs for PUT operations
  • Extending the existing qdn-upload-presign Lambda 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-url to get a presigned URL
  • Thumbnail Strip: Displays recently uploaded photos for the current job, with presigned GET URLs generated by the Lambda's list_photos endpoint

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 integration
  • GET /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