```html

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 .html file 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 .html and 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_KEY hashed with bcrypt in the Lambda environment
  • Bypassing CloudFront (which strips custom headers) by hitting the API Gateway directly
  • Removing sensitive fields like Revenue and Captain Fee from 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