```html

Multi-Domain Guest Experience Architecture: Routing Dynamic Pages Through CloudFront Functions and S3

This post covers the infrastructure and code patterns used to unify guest-facing booking pages across multiple S3-backed domains using CloudFront Functions for dynamic path rewriting, Lambda-backed APIs for presigned uploads, and coordinated event management across isolated service boundaries.

The Problem: Multiple Domains, Single Guest Experience

We operate charter bookings across two primary domains:

  • sailjada.com — internal operations, crew scheduling, fleet management
  • queenofsandiego.com — public-facing brand site

Guest booking confirmations needed to live at guest-friendly URLs like /g/friendly-slug rather than booking IDs, but we had no unified routing layer to handle this. Additionally, CloudFront was stripping custom headers from origin requests, breaking inter-service authentication.

Architecture: Three Integration Points

1. CloudFront Function for Path Rewriting

The CloudFront distribution for queenofsandiego.com uses a viewer-request function to rewrite paths before they hit S3:

// CloudFront Function (queenofsandiego.com distribution)
// Runs on every request in the viewer request phase
if (uri matches /^\/g\/[a-z0-9-]+$/) {
  // Rewrite /g/friendly-slug to /g/friendly-slug.html
  // S3 stores flat .html files, not directories
  return object with uri = uri + '.html'
}

Why this pattern? S3 doesn't have true directories. By storing files as flat .html objects and using CloudFront Functions to rewrite the request path, we achieve clean URLs without directory indices or complicated S3 bucket structures. The function runs at edge locations with ~1ms latency—negligible for path transformation.

2. S3 Bucket Structure

Guest pages are uploaded as flat objects to the queenofsandiego.com bucket:

s3://queenofsandiego.com/g/boatsetter-may-30-2025.html
s3://queenofsandiego.com/g/internal-charter-jun-01.html

The CloudFront function intercepts requests to /g/boatsetter-may-30-2025 and rewrites them to /g/boatsetter-may-30-2025.html before S3 receives the request. This allows clean guest-facing URLs without file extension visibility.

3. Photo Upload Presigning via Lambda

Guest pages include file upload inputs for booking photos. Rather than exposing S3 credentials, we use a Lambda presigner endpoint:

// Route in SCC Lambda (scc-lambda-src/lambda_function.py)
POST /g/{event_id}/presign

// Lambda reads the event_id from the path parameter
// Validates the request came from a known guest
// Returns a presigned POST URL valid for 1 hour
// Guest browser POSTs directly to S3 with the presigned credentials

Why this pattern? Presigned URLs keep S3 credentials off the client while giving guests time-limited upload capability. The Lambda validates that a request is legitimate (event exists, guest IP is reasonable) before issuing credentials. This prevents abuse while maintaining simplicity—no separate file service needed.

Cross-Service Authentication: Bypassing CloudFront Header Stripping

Initial attempts to create SCC events from the calendar Lambda failed silently because CloudFront was stripping the X-Dashboard-Token authentication header. CloudFront's default behavior is to forward only whitelisted headers to origins.

Solution: Call the SCC service directly via API Gateway instead of through CloudFront:

// Instead of:
https://sailjada.com/api/events  (goes through CF, headers stripped)

// Use:
https://{api-id}.execute-api.us-west-2.amazonaws.com/prod/events
(API Gateway origin, all headers forwarded)

This required finding the API Gateway endpoint in CloudFormation outputs and updating the calendar Lambda's HTTP client to use the direct URL. API Gateway preserves all headers by default, so authentication headers now flow through.

Event Creation: Orchestrating Three Systems

A single booking triggers updates across three isolated systems:

  • SCC (ShipCaptainCrew) — crew scheduling, magic link generation, crew notifications
  • JADA Calendar — internal captain calendar and resource blocking
  • S3 + CloudFront — guest-facing confirmation page

The flow:

1. Booking detected (via Boatsetter webhook or manual entry)
2. Create SCC event (POST /events with service key auth)
   → SCC returns event ID and crew magic links
   → SCC automatically notifies crew via email
3. Create JADA Calendar entry (POST /calendar with dashboard token auth)
   → Blocks captain's time
   → Visible internally only
4. Render guest page HTML template
   → Inject event ID, friendly slug, crew details
   → Upload to S3 at s3://queenofsandiego.com/g/{slug}.html
5. Send crew confirmation email
   → Includes links to crew page with checklist
   → Separate from SCC's automatic magic link notification

Why three separate systems? They have different audiences and auth models. SCC is crew-facing (magic links, push notifications). JADA is captain-facing (internal calendar). S3 is guest-facing (public URL, no auth). Keeping them separate prevents credential sprawl and makes each system independently testable.

Data Isolation and Security

Revenue information (what you're getting paid) is explicitly removed from SCC event notes before the event is saved:

// In event payload sent to SCC:
{
  "title": "Charter: Boatsetter May 30",
  "description": "Guest: John Doe, +1-555-0123",
  // ✓ Crew cannot see $840.75 gross or $289.41 net
  // ✓ Captain fee removed before crew notification
  "crew_needed": 2,
  "duration_hours": 5
}

Crew members don't need financial details. By omitting this from SCC, we prevent information leakage if crew members screenshot messages or if the system is compromised.

Key Infrastructure Resources

  • S3 bucket: queenofsandiego.com
  • CloudFront distribution for queenofsandiego.com (with viewer-request function)
  • SCC Lambda: scc-lambda-src/lambda_function.py
  • Calendar Lambda: called from update_dashboard.py tools script
  • API Gateway endpoint: direct invoke URL for auth header preservation
  • DynamoDB table for event storage (accessed by SCC Lambda)

Deployment and Testing

The guest page template is built as a self-contained HTML file with:

  • Embedded CSS (no external stylesheets to load)
  • Event ID injected at template render time
  • Photo upload form configured with the