Multi-Domain Guest Page Architecture: Routing Dynamic Content Through CloudFront Functions and S3
This post covers the infrastructure and code patterns used to build a guest-facing booking page that lives across multiple domains, uses CloudFront Functions for path rewriting, and integrates with a custom Lambda-based event management system. The challenge: serve a single guest experience from different domain origins while keeping the backend stateless and cloud-native.
What We Built
A guest booking confirmation page deployed to queenofsandiego.com under the /g/ path prefix, with:
- A flat
.htmlfile served directly from S3 with CloudFront caching - Dynamic path rewriting via CloudFront Functions (not Lambda@Edge)
- Integration with a DynamoDB-backed event system for photo uploads
- Auto-mailing to crew via a separate Lambda that reads SCC events
- Calendar synchronization with a Google Apps Script endpoint
Domain and S3 Architecture
Why queenofsandiego.com instead of sailjada.com?
The original sailjada.com bucket exists in the same AWS account and is primarily an internal tools domain. Moving guest pages to queenofsandiego.com (the public-facing brand) provides better SEO and cleaner separation of concerns: internal crew tools on one domain, guest experiences on another. This required:
- Identifying the queenofsandiego.com CloudFront distribution ID
- Locating the origin S3 bucket (also named
queenofsandiego.com) - Reading the existing CloudFront Function logic to understand path conventions
S3 Object Paths and Naming Convention
The CloudFront Function for queenofsandiego.com includes logic that rewrites incoming requests. When a request arrives at /g/XHQGMDH, the function must map that to a real S3 object key. The convention we followed:
/g/{friendly-slug}.html
For example, a May 30 Boatsetter charter maps to a slug like boatsetter-5-30-xhqgmdh, stored as:
s3://queenofsandiego.com/g/boatsetter-5-30-xhqgmdh.html
This flat-file approach avoids index.html path rewrites and keeps cache invalidation simple — one file path, one CloudFront invalidation.
CloudFront Function Logic
The existing CloudFront Function (read-only viewer request handler) examines the request URI and applies transformations. Key insight: CloudFront Functions execute before the origin request reaches S3, so they can rewrite paths with minimal latency.
The function checks:
- Does the path start with
/g/? - Is there a query string (e.g.,
?action=upload)? - Route photo presign requests to a Lambda origin instead of S3
- Otherwise, append
.htmland forward to S3
This pattern keeps the static content in S3 (fast, cheap) while photo uploads route through Lambda (where we can validate, hash, and presign upload URLs to DynamoDB-tracked records).
Integration with SCC Lambda and Event System
Event Creation and Crew Auto-Notification
The ShipCaptainCrew (SCC) Lambda at /tmp/scc-lambda-src/lambda_function.py handles event CRUD. Creating an event via the POST /events route automatically triggers SNS notifications to all crew with magic login links. This required:
- Authenticating to the SCC API using a
SERVICE_KEYhashed with bcrypt in the Lambda environment - Bypassing CloudFront (which strips custom headers) by hitting the API Gateway directly
- Removing sensitive fields like
RevenueandCaptain Feefrom event notes before crew see them
Why Direct API Gateway?
CloudFront was stripping the X-Service-Key header, causing auth failures. The solution: construct the API Gateway URL directly (https://{api-id}.execute-api.us-west-1.amazonaws.com/prod/events) instead of routing through CloudFront. This added latency is negligible for crew notifications, which run async.
DynamoDB Updates for Revenue Hiding
Rather than adding complexity to the Lambda response logic, we directly patched the DynamoDB record post-creation using the update expression:
SET #notes = :notes REMOVE Revenue, #captain_fee
This ensures crew never see earning details, even if they query the API directly.
Guest Page HTML Structure
The generated HTML file includes:
- Booking summary: Date, time, location, guest name, charter type
- Photo upload widget: Calls
/g/{slug}?action=upload, which Lambda routes to a presigned S3 URL - Crew confirmation checklist: Populated dynamically from the SCC event record (accessed via magic login link)
- Static CSS and inline JS: No external dependencies, fast load
The page is generated server-side (in the deployment script) rather than client-rendered, ensuring it's SEO-friendly and loads instantly in low-bandwidth conditions.
Deployment and Invalidation
After uploading the .html` file to S3:
aws s3 cp jada-guest-xhqgmdh.html \
s3://queenofsandiego.com/g/boatsetter-5-30-xhqgmdh.html \
--content-type text/html
CloudFront cache was invalidated immediately:
aws cloudfront create-invalidation \
--distribution-id {DIST_ID} \
--paths "/g/boatsetter-5-30-xhqgmdh.html"
Why invalidate immediately? Guests receive the URL via email, and we want them to see the live version instantly. TTL is set to 3600 seconds (1 hour) — long enough for repeat visits, short enough for live updates if we patch the page.
Calendar Integration
A POST to a Google Apps Script endpoint creates an internal calendar entry. This required finding the correct auth token for the dashboard Lambda and formatting the request payload. The calendar is used internally to track crew availability and avoid double-bookings.
Email Workflow
Crew receive two emails:
- SCC auto-notification: Magic link to the event in the crew dashboard
- Captain summary email: Sent via SES to a configured email address, with a summary of the booking and crew assignments
Both are sent asynchronously after the event is created, so the booking confirmation API returns immediately