Implementing Presigned URLs and Multi-Channel Uploads for QuickDumpNow Dashboard

This session focused on enabling three critical workflows for the QuickDumpNow platform: on-the-go photo capture with presigned uploads, Stripe-integrated booking flows, and GPS-aware mobile shortcuts. The technical work centered on extending the Lambda function that powers dashboard.quickdumpnow.com to issue presigned S3 URLs, refactoring the upload pipeline, and staging new booking interfaces.

What Was Done

The primary deliverables were:

  • Added /upload-url endpoint to the Lambda function at /repos/sites/dashboard.quickdumpnow.com/lambda/lambda_function.py to issue presigned POST URLs for direct S3 uploads
  • Extended the /list_photos endpoint to issue presigned GET URLs, enabling secure CloudFront-free image retrieval from private S3 buckets
  • Wired on-the-go capture buttons into the job drawer UI in index.html with thumbnail preview strips
  • Deployed Stripe booking flow with payment method selection (credit card + Zelle picker) to staging at dashboard.quickdumpnow.com/booking
  • Created tracking infrastructure for iOS GPS shortcut integration
  • Stripped macOS extended attributes (xattr) from deployment files to resolve read-permission sandboxing issues

Technical Architecture: S3 Presigned URLs Strategy

The core decision was to avoid exposing the S3 bucket publicly. Instead, the Lambda function acts as a trusted intermediary that issues time-limited presigned URLs. Here's the pattern:


# Presigned POST for uploads (customer uploads to S3 directly)
POST /api/upload-url
Request: { "job_id": "...", "filename": "photo.jpg" }
Response: {
  "url": "https://qdn-uploads.s3.amazonaws.com/",
  "fields": {
    "key": "jobs/[job_id]/[uuid]/photo.jpg",
    "policy": "[base64_policy]",
    "signature": "[hex_signature]",
    "x-amz-credential": "[credential_string]",
    "x-amz-date": "[iso8601_timestamp]",
    "x-amz-algorithm": "AWS4-HMAC-SHA256"
  },
  "expires_in": 3600
}

This presigned POST approach (vs. presigned GET) allows the dashboard UI to upload directly to S3 without the Lambda becoming a passthrough proxy, reducing latency and Lambda execution time. The IAM role attached to the Lambda (qdn-lambda-role) has s3:PutObject permissions scoped to the qdn-uploads bucket.

For retrieving photos via the customer track page, presigned GET URLs are issued server-side:


# Presigned GET for photo display
GET /api/list_photos?job_id=...
Response: [
  {
    "key": "jobs/[job_id]/[uuid]/photo.jpg",
    "url": "https://qdn-uploads.s3.amazonaws.com/jobs/[job_id]/[uuid]/photo.jpg?X-Amz-Algorithm=...",
    "expires_in": 3600
  }
]

These GET URLs bypass CloudFront entirely, keeping the S3 bucket policy restrictive and authentication centralized in the Lambda.

Infrastructure: IAM, S3, API Gateway, and Lambda

S3 Buckets:

  • qdn-uploads — Private bucket for job photos (no public ACL, no bucket policy allowing public reads)
  • dashboard.quickdumpnow.com — Static site hosting with CloudFront distribution; receives dashboard HTML/JS
  • quickdumpnow.com — Root domain bucket serving public landing page and service area pricing pages

Lambda Configuration:

  • Function: qdn-dashboard-lambda (or equivalent, deployed to /repos/sites/dashboard.quickdumpnow.com/lambda/)
  • Runtime: Python 3.11
  • IAM Role: qdn-lambda-role with inline/attached policies for S3 bucket access, Stripe API calls, and CloudFront invalidation
  • Environment Variables: STRIPE_SECRET_KEY (for booking), AWS_REGION, CLOUDFRONT_DIST_ID (for staging invalidation)

API Gateway Routes:

The session added new routes to the existing API Gateway that triggers the Lambda:

  • POST /api/upload-url — Issue presigned POST URLs
  • GET /api/list_photos — Issue presigned GET URLs and job metadata
  • POST /api/book/quote — Stripe quote calculation (added in this session)
  • POST /api/book/confirm — Process payment with Stripe (added in this session)
  • GET /api/book/methods — Return available payment methods (credit card vs. Zelle)

Key Technical Decisions

1. Presigned URLs Instead of CloudFront Public Distribution: Keeping the S3 bucket private allows granular access control at the Lambda layer. Each presigned URL expires after 3600 seconds (1 hour), reducing the window for credential leakage. The Lambda can enforce business logic — e.g., only the job owner can list photos for a specific job ID.

2. Presigned POST for Uploads: Rather than having the UI POST to the Lambda (which would then proxy to S3), we issue presigned POST credentials. This allows direct browser-to-S3 uploads without Lambda becoming a throughput bottleneck. The Lambda only validates the request and generates the signature.

3. Extended Attributes (xattr) Stripping: During deployment, files copied from the macOS working directory carried extended attributes that caused read failures in the Lambda environment. The fix involved stripping these using cp -X or Python's shutil.copy2(), ensuring consistent behavior across dev and prod environments.

4. Staging Validation Before Production Deploy: New Lambda versions and UI changes were validated on a staging CloudFront distribution before pushing to production. The staging deployment invalidated the CloudFront cache to ensure fresh content was served:


# Example staging deployment
aws s3 cp /tmp/lambda_function.py s3://dashboard.quickdumpnow.com/staging/lambda/
aws cloudfront create-invalidation --distribution-id [DIST_ID] --paths "/staging/*"

5. Stripe Integration Without Exposing Secret Keys: The Lambda stores the Stripe secret key as an environment variable, never exposed to the browser. The frontend calls /api/book/quote with service area and duration; the Lambda computes pricing via Stripe's API and returns a quote amount. Payment confirmation also happens server-side to prevent key exposure.

Deployment Process

The session followed this deploy sequence:

  1. Edit Lambda function locally at /Users/cb/