Multi-Domain Guest Booking Pages: CloudFront Path Rewriting and S3 Static Hosting at Scale
This post documents the infrastructure and deployment pattern used to serve guest-facing booking confirmation pages across multiple domains, with a focus on CloudFront path rewriting, S3 static hosting conventions, and cross-domain content organization.
What We Built
The requirement was to create a guest-facing booking confirmation page for a Boatsetter charter with minimal crew overhead. Instead of building a dynamic web application, we leveraged existing infrastructure:
- Static HTML guest page generated and uploaded to S3
- CloudFront distribution with path rewriting rules to serve flat HTML files
- Automatic crew notifications via Lambda-driven event creation
- Multi-domain support (sailjada.com → queenofsandiego.com migration)
The core insight: guest pages don't need dynamic rendering when the data is baked into static HTML at upload time.
S3 Bucket Strategy and Path Conventions
Two S3 buckets were involved in this workflow:
sailjada.com— Primary asset bucket for crew management system (ShipCaptainCrew)queenofsandiego.com— Guest-facing domain for charters
The CloudFront distribution for queenofsandiego.com includes a CloudFront Function that rewrites incoming requests. For guest pages, the convention is:
Request path: /g/{BOOKING_ID}
S3 key stored as: /g/{BOOKING_ID}.html
CloudFront Function rewrites: /g/{BOOKING_ID} → /g/{BOOKING_ID}.html
This allows URLs like https://queenofsandiego.com/g/XHQGMDH to serve the static file without exposing the .html extension. The CloudFront Function (custom logic layer) handles this mapping before hitting S3 origin.
Infrastructure Setup
CloudFront Distribution Configuration:
- Origin: S3 bucket
queenofsandiego.com - Origin Access Control (OAC): Enabled to restrict direct S3 access
- CloudFront Function: Attached to viewer request event
- Cache behavior: Default TTL 3600s (1 hour) for guest pages
CloudFront Function Code Pattern:
// Pseudo-code showing the path rewriting logic
if (request.uri.startsWith('/g/')) {
var parts = request.uri.split('/');
var bookingId = parts[2];
request.uri = '/g/' + bookingId + '.html';
}
return request;
This function is deployed to the CloudFront distribution and executes at the edge, before the request reaches S3 origin. It's language-agnostic and has sub-millisecond latency.
File Organization and Upload Workflow
The guest page HTML is generated locally at:
/tmp/jada-guest-{BOOKING_ID}.html
It contains embedded:
- Charter details (date, time, location, crew roster)
- Guest instructions and safety briefing
- Pre-signed S3 photo upload endpoints (for guest photos during charter)
- Booking confirmation metadata
The upload command:
aws s3 cp /tmp/jada-guest-{BOOKING_ID}.html \
s3://queenofsandiego.com/g/{BOOKING_ID}.html \
--content-type text/html \
--cache-control "public, max-age=3600"
Following S3 best practices:
- Explicit Content-Type prevents browser interpretation as binary
- Cache-Control header tells CloudFront and browsers how long to cache
- Flat filename (
.html) avoids directory index ambiguity
Why This Architecture Over Alternatives
Static vs. Dynamic: Dynamic rendering (Lambda@Edge, CloudFront origin custom headers) adds cold-start latency and complexity. Guest pages are requested once per booking during confirmation — the data never changes. Static HTML with embedded data baked in at generation time is the right tool.
Domain Migration (sailjada.com → queenofsandiego.com): Initially, guest pages were staged at sailjada.com/g/{BOOKING_ID}. We migrated to queenofsandiego.com because:
- Guests associate the brand (Queen of San Diego) with the charter experience, not internal tools
- Reduced CORS and authentication complexity (guests don't need SCC credentials)
- Cleaner analytics — guest traffic separated from crew/admin traffic
- Future SSL certificate and subdomain isolation for different charter brands
Crew Notification and Event Creation
The guest page upload is coupled with an event creation in ShipCaptainCrew's DynamoDB table. A Lambda function (triggered by the booking workflow) calls:
POST /events
{
"eventId": "{BOOKING_ID}",
"date": "2024-05-30",
"duration": 3,
"crewCount": 2,
"notes": "Charter details...",
"autoNotify": true
}
The autoNotify: true flag triggers the SCC Lambda to generate magic links and send notification emails to all assigned crew. Magic links are time-expiring tokens that let crew confirm availability without needing to log in.
Why magic links? Crew often use disposable phones or haven't set up accounts. Magic links reduce friction to zero — they click the link, confirm or decline in 10 seconds, done.
Key Decisions and Trade-offs
- No crew UI generated: This charter used bare-minimum notification. Crew got an email with event details and a magic link. No dedicated crew landing page was created — that's a separate feature tracked in the kanban board.
- Revenue hidden from crew: The event notes initially included revenue data. We explicitly removed it via DynamoDB update before crew notifications, because crew shouldn't see how much the charter grosses — that's internal business data. The update was made directly to DynamoDB rather than through the Lambda API to ensure immediate consistency.
- No captain fee duplication: Boatsetter may include a captain fee in the charter price; we confirmed the net-to-owner calculation locally but didn't add a second captain fee in the SCC event. The Lambda event creation accepts a
captainFeefield, but it's optional and only used for internal accounting when the guest is paying the crew directly (not via Boatsetter).
Deployment and Verification
After upload to S3 and CloudFront invalidation:
curl -I https://queenofsandiego.com/g/XHQGMDH
Expected response: