```html

Multi-Domain Guest Page Architecture: Routing Charter Bookings Across CloudFront Distributions

When a charter books through Boatsetter, we need to spin up infrastructure across three separate domains with different ownership and CloudFront configurations. This post walks through the technical decisions that let us automate guest pages, crew notifications, and internal calendar entries from a single booking event.

The Problem: Three Domains, One Booking

A Boatsetter charter creates a need for:

  • sailjada.com — internal crew scheduling and event management
  • queenofsandiego.com — public-facing guest experience and photo uploads
  • JADA Internal Calendar — captain and operations visibility

Each domain runs on different S3 buckets with distinct CloudFront configurations. The guest page needs to live at queenofsandiego.com/g/FRIENDLY_SLUG, while the crew-facing checklist and photo upload integration lives in the ShipCaptainCrew (SCC) Lambda and DynamoDB. Automating this required understanding three different authentication mechanisms and CloudFront function behaviors.

Architecture Overview

Data Flow:

  • Boatsetter webhook → Lambda trigger
  • Create SCC event (DynamoDB) + auto-email crew with magic links
  • Create JADA Internal Calendar entry with dashboard Lambda
  • Generate static guest page HTML
  • Upload to queenofsandiego.com S3 bucket
  • Invalidate CloudFront cache for immediate visibility

Key Technical Decisions

1. Guest Page Hosting: S3 Bucket Structure and CloudFront Path Rewriting

We chose to host the guest page as a flat .html file rather than a directory structure, based on the CloudFront function already configured for queenofsandiego.com.

CloudFront Function Logic (existing):


// Rewrites /g/SLUG to /g/SLUG.html
// Maps public guest URL to S3 object naming convention
if (uri.startsWith('/g/')) {
  return uri.endsWith('.html') ? uri : uri + '.html';
}

This meant uploading to S3 as /g/jada-guest-xhqgmdh.html and serving it at queenofsandiego.com/g/jada-guest-xhqgmdh. The function automatically appends .html, so visitors don't see the extension.

S3 Bucket: queenofsandiego.com (not sailjada.com)

CloudFront Distribution ID: Found via AWS CLI against the queenofsandiego.com domain.

Invalidation command (no credentials shown):


aws cloudfront create-invalidation \
  --distribution-id [DIST_ID] \
  --paths "/g/jada-guest-xhqgmdh*"

2. Authentication Across Three APIs

We discovered three different auth mechanisms in use:

  • Dashboard Lambda (calendar entries): Custom X-Dashboard-Token header. Token is hashed with SHA-256 and stored in Lambda env vars.
  • ShipCaptainCrew (SCC) event creation: Service key auth. Initially failed because CloudFront was stripping custom headers. Solution: hit the API Gateway directly instead of routing through CloudFront.
  • SCC guest page photo uploads: Magic link presigning. The guest page embeds a presigned S3 URL for photo upload, generated server-side by the /g/PRESIGN route in SCC Lambda.

Why we switched to direct API Gateway URLs: CloudFront function was stripping the Authorization header, so service key auth failed. By calling the API Gateway endpoint directly (bypassing CloudFront origin), we preserved headers.

3. Separating Public and Internal Data in SCC Events

A critical requirement: crew should see their tasks and the charter details, but not the revenue split. The initial event notes included:


Revenue: $840.75
Captain Fee: $150.00
Crew Cost: $250.00
Net to Captain: $289.41

This is sensitive business data. Rather than building a separate "crew-safe" event model, we:

  1. Created the SCC event with full financials in DynamoDB
  2. Patched the event to remove revenue/cost fields from the notes field before crew notification
  3. Used DynamoDB direct updates (via the Lambda code path that handles event updates) to strip sensitive fields

Event update route: PATCH /events/{eventId} in /tmp/scc-lambda-src/lambda_function.py

Handler function: update_event() validates the service key, updates the item in DynamoDB, and returns the updated event.

4. Guest Page as a Templated HTML File, Not a Lambda Redirect

We could have made the guest page dynamic (Lambda function), but instead opted for static HTML because:

  • Performance: CloudFront caches static files at edge locations globally.
  • Simplicity: No Lambda cold start on guest page loads.
  • Photo upload integration: The guest page contains a presigned S3 URL (generated at build time) that expires after 24 hours. Guests can upload photos without additional auth.

The guest page is generated with a Python script that:

  1. Reads the booking details from the SCC event (charter date, guest names, location)
  2. Calls the SCC /g/PRESIGN endpoint to get a time-limited S3 upload URL
  3. Embeds this URL in the HTML form
  4. Uploads the rendered HTML to S3

Infrastructure & Resource Names

  • Primary S3 buckets: sailjada.com, queenofsandiego.com
  • Lambda functions: Dashboard (calendar), SCC event handler, Boatsetter webhook processor
  • DynamoDB table: ShipCaptainCrew (stores events, crew assignments, photos)
  • CloudFront distributions: One for each domain, with function associations on the /g/ path
  • API Gateway: Separate endpoint for SCC (to bypass CloudFront header stripping)
  • SES: Email delivery for crew notifications and captain summary

Automation Workflow

A single booking trigger now executes (in parallel where possible):


1. Create SCC event → DynamoDB
   └─ Auto-emails crew with