Multi-Domain Event Routing: Hosting Guest Pages on CloudFront with Path-Based Rewriting
When you operate multiple charter booking domains with different purposes—one for crew operations, one for guest-facing interactions—you need a clean separation without maintaining parallel infrastructure. This post documents how we consolidated guest page hosting across domains using CloudFront Functions for intelligent path rewriting, keeping infrastructure costs down while maintaining strict domain boundaries.
The Problem: Domain Fragmentation
We were originally hosting guest pages on sailjada.com, but the primary public-facing domain for charter bookings is queenofsandiego.com. Maintaining duplicate infrastructure or managing complex redirects adds operational overhead. The solution: use CloudFront Functions to transparently rewrite requests from queenofsandiego.com/g/* paths to serve pre-built HTML files stored in S3, without visible redirects.
Architecture Overview
The pattern we implemented:
- S3 Origin: Guest pages stored as flat
.htmlfiles insailjada.comS3 bucket - CloudFront Distribution:
queenofsandiego.comdistribution IDE2ABC...(actual ID omitted) - CloudFront Function: Viewer request function that intercepts
/g/paths and rewrites them for S3 origin - Cache Behavior: Separate cache behavior for guest paths with appropriate TTLs and query string handling
CloudFront Function Implementation
The core logic lives in a CloudFront Function (not a Lambda@Edge—CloudFront Functions are simpler, cheaper, and faster for request rewriting):
// Simplified pseudocode of the rewrite logic
function handler(request) {
var uri = request.uri;
// Guest page requests: /g/XHQGMDH → /g/XHQGMDH.html
if (uri.startsWith('/g/') && !uri.endsWith('.html')) {
request.uri = uri + '.html';
}
return request;
}
Why this approach? CloudFront Functions execute in the viewer request phase at edge locations, before origin requests. They're perfect for lightweight path manipulation without the cold-start overhead of Lambda@Edge.
S3 Bucket Organization
Guest pages are uploaded to the sailjada.com S3 bucket with the flat naming convention that the CF Function expects:
s3://sailjada.com/g/XHQGMDH.html
s3://sailjada.com/g/ABC12345.html
s3://sailjada.com/g/DEF67890.html
Each file is a complete, self-contained HTML document with embedded styles and minimal external dependencies. This reduces request latency and simplifies invalidation logic.
Cache Behavior Configuration
We created a dedicated cache behavior for the /g/* path pattern:
- Path Pattern:
/g/* - Origin:
sailjada.comS3 bucket - Viewer Protocol Policy: Redirect HTTP to HTTPS
- Allowed HTTP Methods: GET, HEAD, OPTIONS (guest pages are read-only from CloudFront)
- Cache TTL: 86400 seconds (24 hours) for guest pages; shorter for frequently-updated content
- Query String Forwarding: None (guest page URLs are stable; photo upload presigning happens via Lambda@Edge on a different path)
- Compress Objects Automatically: Enabled (HTML compresses well)
Why separate from the default behavior? The main queenofsandiego.com origin handles dynamic content (crew dashboards, booking forms) with different caching rules. Isolating guest pages lets us apply aggressive caching without compromising dynamic content freshness.
Integration with Event Data
When a new charter is booked (via Boatsetter or manual entry), the system:
- Creates a
ShipCaptainCrewevent entry via Lambda API (authenticated with service key) - Generates a unique guest page identifier (e.g.,
XHQGMDH) - Renders the guest page template with booking details (date, location, special instructions)
- Uploads the rendered HTML to
s3://sailjada.com/g/XHQGMDH.html - Invalidates the CloudFront cache for that path (if needed for immediate delivery)
- Sends guest a URL like
https://queenofsandiego.com/g/XHQGMDH
Key Decision: Why Not Route53 Alias Records?
We considered adding a Route53 alias record directly to the CloudFront distribution, but that would create CNAME conflicts with the existing DNS setup. Instead, the path-based routing approach lets us use the existing queenofsandiego.com CloudFront distribution without DNS changes, reducing blast radius and simplifying rollback.
Handling Photo Uploads
Guest pages include a photo upload widget. Presigned URL generation happens through a separate Lambda@Edge function on the /g/photos/* path, which:
- Validates the guest page ID
- Generates a time-limited S3 presigned POST URL
- Returns the URL as JSON via CORS-enabled response headers
- Photos are uploaded to a separate S3 bucket partition:
s3://sailjada-guest-uploads/XHQGMDH/*
This separation keeps the guest page bucket read-only from CloudFront while allowing guest uploads to a separate bucket with stricter access policies.
Automation and Scaling
The entire flow is triggered automatically when a new booking event is created:
- Trigger: DynamoDB stream on the events table → Lambda function
- Template Rendering: Mustache or similar lightweight templating (no server-side rendering overhead)
- S3 Upload: Direct PUT request with appropriate Cache-Control headers
- Invalidation: CloudFront invalidation API call only if the page already existed (update scenario)
This event-driven approach ensures every guest page is generated fresh, never stale, and scales to any number of concurrent bookings without additional infrastructure.
What's Next
Future improvements we're considering:
- Origin Shield: Enable CloudFront Origin Shield on the
sailjada.comS3 origin to reduce request load during cache misses - Lambda@Edge Analytics: Log guest page accesses and engagement metrics to CloudWatch for booking funnel analysis
- A/B Testing: Use CloudFront Functions to serve variant guest pages based on booking source (Boatsetter vs. direct)
- Dynamic TTLs: Adjust cache TTL