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 .html files in sailjada.com S3 bucket
  • CloudFront Distribution: queenofsandiego.com distribution ID E2ABC... (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.com S3 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:

  1. Creates a ShipCaptainCrew event entry via Lambda API (authenticated with service key)
  2. Generates a unique guest page identifier (e.g., XHQGMDH)
  3. Renders the guest page template with booking details (date, location, special instructions)
  4. Uploads the rendered HTML to s3://sailjada.com/g/XHQGMDH.html
  5. Invalidates the CloudFront cache for that path (if needed for immediate delivery)
  6. 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.com S3 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