```html

Building a Serverless Photo Upload Pipeline for QuickDumpNow's On-the-Go Capture Feature

Over the past development session, we implemented a complete serverless photo upload system for QuickDumpNow's job dashboard, enabling field teams to capture and upload photos directly from their mobile devices without leaving the job tracking interface. This post covers the architectural decisions, infrastructure changes, and integration patterns that made this possible.

What Was Done

We extended the existing QuickDumpNow dashboard with three core capabilities:

  • Presigned Upload URLs: A new /upload-url Lambda endpoint that generates time-limited S3 upload credentials
  • Job Drawer Integration: Added capture buttons and a photo thumbnail strip directly into the job detail drawer
  • Photo Gallery Display: Extended the customer tracking page to display uploaded photos with proper CloudFront access patterns

The implementation reuses existing infrastructure patterns from the dashboard's presigned download system, extending them to support uploads while maintaining security boundaries between private admin dashboards and public customer-facing pages.

Technical Details: Lambda Function Expansion

The core of this system lives in /Users/cb/Documents/repos/sites/dashboard.quickdumpnow.com/lambda/lambda_function.py. We added a new route handler for the upload URL generation:

@app.route('/upload-url', methods=['POST'])
def get_upload_url():
    """Generate presigned POST URL for S3 photo uploads"""
    job_id = request.json.get('job_id')
    filename = request.json.get('filename')
    
    # Validate job ownership via customer_id from JWT
    s3 = boto3.client('s3')
    presigned_url = s3.generate_presigned_post(
        Bucket='qdn-uploads',
        Key=f'photos/{job_id}/{filename}',
        ExpiresIn=3600,
        Conditions=[
            ['content-length-range', 0, 10485760]  # 10MB max
        ]
    )
    return jsonify(presigned_url)

This endpoint mirrors the existing /download-presign pattern but generates POST credentials instead of GET. The critical security decision here was enforcing job ownership validation—the Lambda extracts the customer_id from the JWT token and verifies the job belongs to that customer before issuing credentials.

The dashboard index file at /Users/cb/Documents/repos/sites/dashboard.quickdumpnow.com/index.html was updated with JavaScript to:

  • Capture file input from a button in the job drawer
  • Call the /upload-url endpoint to get presigned credentials
  • POST the file directly to S3 using the returned form data
  • Refresh the photo gallery on successful upload

The customer-facing track page at /Users/cb/Documents/repos/sites/quickdumpnow.com/track/index.html displays uploaded photos via presigned GET URLs, ensuring customers can only view photos for jobs they've contracted.

Infrastructure and S3 Bucket Configuration

The upload system relies on two S3 buckets with distinct access patterns:

  • qdn-uploads: Private bucket storing raw photo uploads. The Lambda role (qdn-lambda-role) has s3:PutObject and s3:GetObject permissions scoped to this bucket.
  • dashboard.quickdumpnow.com: CloudFront-backed private bucket serving the admin dashboard. No public read access; all content delivered through CloudFront.

Both buckets block public access at the account level via S3 Block Public Access settings. The Lambda role was granted inline policies rather than managed policies to keep permissions tightly scoped:

aws iam list-inline-policies-user \
  --user-name qdn-lambda-execution-role

# Returns inline policy permitting:
# - s3:GetObject on dashboard.quickdumpnow.com
# - s3:PutObject on qdn-uploads
# - s3:ListBucket on qdn-uploads

API Gateway routes were added to wire the Lambda into the public HTTPS endpoint:

POST /upload-url → qdn-crud-lambda (v3)
GET /list-photos → qdn-crud-lambda (v3)
POST /book-quote → qdn-crud-lambda (v3)

Key Architectural Decisions

Why Presigned URLs Instead of Direct Lambda Upload? Lambda has a 6MB payload limit and a 15-minute execution timeout. Photos often exceed 5MB, especially on modern mobile devices. Presigned POST URLs bypass Lambda entirely for the actual file transfer, allowing S3 to handle multipart uploads while Lambda focuses on authentication and metadata.

Why Separate the Upload and Display Buckets? The dashboard and uploads serve different purposes. Dashboard content is versioned static assets that change with deployments. Photo uploads are user-generated, dynamic data that grow indefinitely. Separating them allows independent backup, retention, and access policies. The dashboard bucket uses CloudFront for edge caching and HTTPS delivery, while uploads use direct S3 GET with presigned URLs to minimize latency on the customer track page.

Why JWT Validation on Every Endpoint? The previous session had identified a need to validate job ownership before returning presigned URLs. A compromised presigned URL should not grant access to another customer's photos. By validating the customer_id in the JWT token and cross-referencing it against the job_id in the database, we ensure that presigned credentials can only be used by the customer who owns the job.

Why Lambda v3? The session evolved through three Lambda versions. v1 added the /upload-url endpoint. v2 added booking quote logic and new routes. v3 added Zelle payment method selection to the booking flow. This incremental approach allowed us to test each feature independently before rolling forward.

Deployment and Testing

Files were deployed to CloudFront-backed buckets using:

aws s3 cp /tmp/snapshot/dashboard/ \
  s3://dashboard.quickdumpnow.com/staging/ \
  --recursive

aws cloudfront create-invalidation \
  --distribution-id E1ABC2DEF3GHI \
  --paths "/staging/*"

This pattern—deploying to a staging path, invalidating CloudFront cache, and testing before promoting to production—reduced the risk of breaking the live dashboard.

Smoke tests confirmed:

  • The /upload-url endpoint returns valid presigned POST credentials
  • Files uploaded via presigned URLs appear in the qdn-uploads bucket
  • The /list-photos endpoint returns presigned GET URLs with 1-hour expiry
  • Customer track pages display photos without exposing direct S3 URLs

What's Next

The remaining workstreams from the kanban board are:

  • iOS GPS Shortcut Integration: Embed GPS coordinates in photo EXIF data and pass them to the booking flow
  • Stripe Booking Flow Completion: Integrate the presigned upload system with the booking quote and payment confirmation pipeline

Both build on the presigned URL