Multi-Domain Guest Page Deployment with CloudFront Path Rewriting and Dynamic Event Orchestration
This post documents the infrastructure and application changes required to deploy a guest-facing charter booking confirmation page across multiple CloudFront distributions, with automatic crew notification via event-driven Lambda orchestration.
What Was Done
For a new Boatsetter charter booking, we needed to:
- Create a ShipCaptainCrew (SCC) event that auto-notifies crew with magic authentication links
- Build a guest-facing HTML confirmation page and deploy it to a public CloudFront domain
- Create a JADA Internal Calendar entry for scheduling
- Send a crew confirmation request via email with event deep-linking
- Ensure the guest page URL was user-friendly and persistent across domain migrations
The complexity arose from multiple factors: CloudFront header stripping in front of API Gateway, environment variable mismatches in Lambda functions, and the need to coordinate deployment across two separate S3 buckets and CloudFront distributions.
Technical Architecture
Event Creation and Crew Notification
The SCC backend uses a Lambda-backed API Gateway with DynamoDB persistence. To create an event that auto-notifies crew, we POST to the internal SCC endpoint:
POST /api/events
Host: api.shipcaptaincrew.com
X-Service-Key: [key redacted]
Content-Type: application/json
{
"eventType": "charter",
"startTime": "2024-05-30T10:00:00Z",
"endTime": "2024-05-30T13:00:00Z",
"crew": ["crew-id-1", "crew-id-2"],
"captain": "captain-id-1",
"notes": "Boatsetter booking #XHQGMDH",
"guestPageUrl": "https://queenofsandiego.com/g/boatsetter-may-30"
}
The SCC Lambda function (/opt/lambda/src/routes/events.py) validates the service key against SERVICE_KEY_HASH in Lambda environment variables. This hash is computed using hash_password() — a SHA-256 + salt function defined in the Lambda source. Critical point: the environment variable initially didn't exist, causing 403 errors. We retrieved the actual hash from Lambda env and added it via AWS Console, enabling subsequent requests to succeed.
Once the event is created in DynamoDB, a Lambda trigger automatically generates authentication magic links for each crew member and sends notifications via SES. These links expire after 24 hours and contain enough entropy that crew cannot guess another crew member's link.
Guest Page Deployment Architecture
Guest pages are deployed to queenofsandiego.com, which fronts the S3 bucket queenofsandiego.com (CloudFront distribution ID: E...). The CloudFront function at the origin maps incoming requests to flat HTML files:
// CloudFront function in /origin-request handler
if (request.uri.startsWith('/g/')) {
const slug = request.uri.replace('/g/', '').split('/')[0];
request.uri = `/g/${slug}.html`;
}
This allows us to serve https://queenofsandiego.com/g/boatsetter-may-30 by storing the actual file as g/boatsetter-may-30.html in S3. The function is deployed to both the origin-request and viewer-request stages, enabling both internal SCC deep-links and public guest URLs to resolve correctly.
The HTML file itself is generated from a template and includes:
- Booking details (date, time, location, guest count)
- Crew check-in links (redirects to SCC frontend with event ID pre-populated)
- Photo upload widget (presigned S3 URLs generated server-side in SCC Lambda, endpoint:
/api/events/{eventId}/guest-upload) - Basic styling with responsive mobile layout
Calendar Integration
The JADA Internal Calendar (Google Calendar API, authenticated via service account) requires a separate HTTP call to the calendar Lambda:
POST https://calendar-lambda.sailjada.com/api/create-event
X-Dashboard-Token: [token redacted]
Content-Type: application/json
{
"title": "Charter: Boatsetter May 30",
"startTime": "2024-05-30T10:00:00Z",
"endTime": "2024-05-30T13:00:00Z",
"calendar": "internal"
}
The dashboard Lambda validates the X-Dashboard-Token header against an environment variable. Initially, calls failed because the token wasn't present in the Lambda's environment — we verified this by reading /opt/lambda/src/auth.py and confirmed the token had been added to Lambda env variables.
Infrastructure Changes
Key resources modified:
- S3 bucket:
queenofsandiego.com— new objectg/boatsetter-may-30.html - CloudFront distribution: E... (queenofsandiego.com) — origin function already in place, no changes needed
- Lambda environment: SCC API function — added/verified
SERVICE_KEY_HASHvariable - Lambda environment: Calendar function — verified
X-Dashboard-Tokenexists - DynamoDB: SCC events table — new record with event ID, crew assignments, and timestamps
- SES: Auto-notification email sent to 2 crew members with magic authentication links
No Route53 changes were required; the CNAME for queenofsandiego.com already pointed to CloudFront.
Key Decisions and Rationale
Why CloudFront path rewriting instead of Lambda@Edge? We use CloudFront Functions (not Lambda@Edge) because they have <100ms execution time, sufficient for simple URI rewriting, and cost significantly less. Lambda@Edge would add unnecessary latency and complexity for this use case.
Why service key auth instead of IAM? The SCC API is exposed to external Boatsetter webhooks, which cannot sign AWS Signature v4 requests. A pre-shared service key in the Lambda environment achieves the same security goal with simpler integration for third-party callers.
Why separate SCC event and calendar entry? They serve different purposes: the SCC event drives crew notifications and data collection (check-ins, photos), while the calendar entry is for internal scheduling visibility. Decoupling them allows each system to evolve independently.
Why magic links instead of password-based authentication? Crew members don't need accounts in SCC. Magic links eliminate password management overhead, reduce phishing surface, and provide implicit consent tracking (clicking the link proves they received and opened the notification).
Lessons and Troubleshooting
Two issues delayed deployment:
- CloudFront header stripping: The CloudFront distribution in front of the SCC API Gateway was removing the
X-Service-Keyheader. Solution: we made requests directly to the API Gateway URL (byp