```html

Multi-Domain Guest Page Architecture: Routing Boatsetter Charters Across S3 + CloudFront + Lambda

This post covers the infrastructure decisions made to handle a new Boatsetter charter booking workflow, including guest-facing pages, crew notifications, and event management across two separate domains with different hosting models.

What Was Done

We implemented an end-to-end booking pipeline for a 3-hour charter that required:

  • A ShipCaptainCrew (SCC) event creation with crew auto-notifications via magic links
  • A JADA Internal Calendar entry for scheduling verification
  • A guest-facing page at a friendly URL (/g/ prefix) to collect pre-charter information and handle photo uploads
  • SES email notification to crew members requesting confirmation
  • Multi-domain hosting: pages initially built for sailjada.com, then migrated to queenofsandiego.com

The guest page had to work across two different S3 + CloudFront configurations with distinct path-rewriting rules, requiring careful attention to URL routing and authentication mechanisms.

Technical Architecture

Domain Routing and CloudFront Configuration

Initially, the guest page was uploaded to the sailjada.com S3 bucket (resource: s3://sailjada.com) with CloudFront distribution ID E1234ABCD. However, the business requirement changed to host guest pages on queenofsandiego.com instead.

queenofsandiego.com uses a more sophisticated CloudFront architecture with a CloudFront Function (not Lambda@Edge) that rewrites incoming requests. The function applies path-based routing logic:

// CloudFront Function routing logic
if (request.uri.startsWith('/g/')) {
  // Rewrite /g/SLUG to /g/SLUG.html
  request.uri = request.uri + '.html';
}

This means guest pages must be uploaded as flat .html files (e.g., g/xhqgmdh.html) rather than directory-based structures. The function strips CloudFront headers before forwarding to the origin, which had implications for API authentication (more on that below).

S3 Bucket Structure

The queenofsandiego.com S3 bucket stores guest pages in a predictable location:

s3://queenofsandiego.com/g/<booking-id>.html

We uploaded the generated HTML directly to this path without a directory wrapper. The CloudFront Function then rewrite request URIs to append .html, allowing clean URLs like https://queenofsandiego.com/g/xhqgmdh.

SCC Event Creation and Authentication

ShipCaptainCrew events are created via Lambda, protected by a service key hash mechanism. The SCC Lambda function environment stores:

  • SERVICE_KEY_HASH: SHA-256 hash of a pre-shared service key
  • Crew and admin credentials for internal operations
  • DynamoDB table references for event and crew data

When creating an SCC event programmatically, the request includes:

POST /scc-api-endpoint
X-Service-Key: <raw_service_key>

{
  "event_type": "charter",
  "date": "2024-05-30",
  "duration_hours": 3,
  "crew_ids": [<crew_id_1>, <crew_id_2>],
  "notes": "Boatsetter booking - crew must confirm"
}

The Lambda handler hashes the incoming key and compares it to the stored hash before proceeding.

Multi-API Authentication Layers

Two separate authentication mechanisms were discovered during implementation:

  • Dashboard Lambda: Protected by X-Dashboard-Token header (calendar operations)
  • SCC Lambda: Protected by X-Service-Key header with SHA-256 hashing (event operations)

Critically, the CloudFront Function for queenofsandiego.com strips custom headers before forwarding to origin. This forced us to bypass CloudFront and hit the SCC API Gateway endpoint directly:

https://<api-gateway-id>.execute-api.us-west-2.amazonaws.com/prod/scc/events

Instead of routing through the CloudFront distribution, preserving the auth headers in transit.

Guest Page HTML Generation

The guest page is a self-contained HTML file with embedded CSS and JavaScript. It includes:

  • Pre-charter information form (guest name, phone, special requests)
  • Photo upload mechanism using S3 presigned URLs
  • Time-aware upload logic (photo submissions only enabled within boarding window)
  • Links to crew pages and captain instructions

Photo uploads are handled via a dedicated Lambda route (/g/presign) that generates time-limited S3 URLs:

GET /g/presign?booking_id=xhqgmdh&file_type=png

Response:
{
  "upload_url": "https://s3.us-west-2.amazonaws.com/queenofsandiego.com/...",
  "expires_in_seconds": 3600
}

File uploads are stored in a booking-scoped S3 prefix for isolation and cleanup.

Infrastructure and Deployment

CloudFront Invalidation

After uploading to S3, we invalidate the CloudFront cache:

aws cloudfront create-invalidation \
  --distribution-id <DIST_ID> \
  --paths "/g/xhqgmdh.html" "/g/xhqgmdh/*"

This ensures edge caches serve the new version immediately rather than waiting for TTL expiration.

Calendar and Event Synchronization

JADA Internal Calendar entries are created via a separate Lambda with a different auth mechanism. The calendar stores:

  • Booking date and duration
  • Crew and captain assignments
  • Revenue figures (intentionally excluded from crew-facing SCC events)
  • Boatsetter booking reference ID

SCC events created for the same booking intentionally omit revenue data, ensuring crew members don't see earnings information when they access magic links.

Email Notification Pipeline

Crew notifications are sent via SES with a BCC to the owner (CB). The crew members receive:

  • Magic link to SCC event page with confirmations and checklist
  • Booking date, duration, and vessel information
  • No revenue or financial data

This is handled by a Google Apps Script endpoint that triggers S