Building a Multi-Domain Guest Experience: CloudFront Path Rewriting, Lambda Authentication, and S3 Asset Management
This post documents the infrastructure and application changes required to support guest-facing booking pages across multiple domains, with special focus on CloudFront functions, Lambda authentication patterns, and cross-domain asset hosting decisions.
What Was Done
We built a complete guest experience workflow for a charter booking, including:
- Guest-facing HTML page served from
queenofsandiego.com/g/path - Crew confirmation page with checklist functionality
- JADA Internal Calendar event for booking tracking
- ShipCaptainCrew (SCC) event creation with auto-notification to crew
- Email notification to crew requesting confirmation
- Multi-domain asset hosting strategy (S3 bucket selection and CloudFront distribution routing)
Technical Details: CloudFront Path Rewriting Strategy
The primary challenge was deciding where to host guest pages. We initially hosted at sailjada.com/g/BOOKINGID, but moved to queenofsandiego.com/g/BOOKINGID for brand consistency.
The CloudFront distribution for queenofsandiego.com uses a CloudFront Function to rewrite paths. The function intercepts requests to /g/* and maps them to flat HTML files in S3:
Request: GET /g/XHQGMDH
CloudFront Function rewrites to: /g/XHQGMDH.html
S3 lookup: s3://queenofsandiego.com/g/XHQGMDH.html
This convention is critical because S3 doesn't natively support directory-style routing. By storing files as /g/[booking-id].html and using CloudFront Functions to append the .html extension, we present clean URLs to guests while maintaining S3 compatibility.
Why CloudFront Functions instead of Lambda@Edge? CloudFront Functions have lower latency (execute at edge locations globally) and handle simple path rewriting more efficiently than Lambda@Edge, which executes from regional endpoints. For a read-only path transformation, Functions are the right choice.
Infrastructure: S3 Bucket and CloudFront Configuration
Guest pages are stored in the queenofsandiego.com S3 bucket with this structure:
s3://queenofsandiego.com/
├── g/
│ ├── XHQGMDH.html (guest page for booking XHQGMDH)
│ ├── FRIENDLY-BOOKING-NAME.html (human-readable booking slug)
│ └── [other booking IDs].html
├── assets/
│ ├── css/
│ ├── js/
│ └── images/
└── index.html
The CloudFront distribution serves this bucket with:
- Origin:
queenofsandiego.com.s3.amazonaws.com - Origin path: (root)
- CloudFront Function: Attached to viewer request for path rewriting
- Cache behaviors: TTL set to 300 seconds for guest pages (allows updates without waiting for expiration)
After uploading a guest page HTML file to S3, we invalidate CloudFront cache using the distribution ID to ensure the new version serves immediately:
aws cloudfront create-invalidation \
--distribution-id E[DISTRIBUTION_ID] \
--paths "/g/XHQGMDH" "/g/XHQGMDH.html"
We invalidate both the clean path and the .html extension to account for any caching permutations.
Lambda Authentication: Handling CloudFront Header Stripping
A critical issue emerged during development: CloudFront strips custom headers when forwarding requests to origin Lambda functions (API Gateway). This prevented authentication headers from reaching the backend.
The Problem:
- Guest page creation requires calling backend APIs (calendar Lambda, SCC Lambda)
- Both APIs use custom authentication headers (
X-Dashboard-Token, service key hashing) - CloudFront strips these headers before reaching the origin
The Solution: Use the direct API Gateway endpoint instead of the CloudFront distribution:
// Instead of:
https://dashboard.sailjada.com/calendar
// Use:
https://[api-gateway-id].execute-api.us-west-2.amazonaws.com/prod/calendar
This bypasses CloudFront entirely for API calls, preserving authentication headers. The tradeoff: we lose caching and DDoS protection on these endpoints, but internal API calls are low-volume and this is an acceptable security boundary (internal auth tokens are not exposed to public routes).
For SCC specifically, the Lambda environment variable SERVICE_KEY_HASH stores the bcrypt hash of the service key. The Lambda verifies incoming service keys by:
- Extracting the service key from the request header
- Hashing it with bcrypt
- Comparing to
SERVICE_KEY_HASHfrom environment - Allowing or rejecting the request
This pattern prevents storing plaintext keys in logs or configuration.
Guest Page Generation: Minimal and Extensible
The guest page is a single-file HTML document with inline CSS and JavaScript. It contains:
- Charter details (date, time, location, guest count)
- Photo upload functionality (pre-signed S3 URLs generated by SCC Lambda)
- Links to crew confirmation pages (for crew without hostess/host roles)
- Styling for mobile-first responsive design
The page calls the SCC /g/[booking-id]/presign endpoint to get pre-signed POST URLs for photo uploads. This Lambda handler:
// SCC Lambda: handle_guest_presign()
1. Validates guest ID matches booking
2. Generates S3 pre-signed POST URL (valid for 1 hour)
3. Returns URL and S3 bucket details to guest
4. Guest uploads directly to S3, bypassing Lambda
Direct S3 uploads from the guest browser reduce Lambda invocations and latency — the guest's bandwidth uploads directly to S3 rather than flowing through Lambda.
Event Creation and Crew Notification
Creating an SCC event triggers automatic email notifications to all assigned crew. The workflow:
- Guest page creation calls SCC event creation endpoint
- Lambda creates DynamoDB entry with crew list
- SCC Lambda generates magic auth links for each crew member
- SES (Simple Email Service) sends emails with crew-specific links
- Crew clicks link → authenticated session → crew page with checklist
Magic link auth prevents password friction for crew and ensures each crew member sees only their assigned booking.
Key Decisions and Rationale
Why flat HTML files instead of dynamic generation? Guest pages don't change during a