```html

Multi-Domain Guest Page Architecture: CloudFront Path Rewriting and Cross-Origin S3 Deployment

During a recent development session, we faced a challenge common in multi-tenant booking platforms: how to serve guest-facing charter confirmation pages across multiple domains while maintaining a single source of truth for infrastructure. This post documents the technical decisions, architecture patterns, and deployment strategy we used to solve it.

The Problem: Domain Fragmentation and Path Routing

Our system serves charter bookings through multiple brands — sailjada.com (internal operations), queenofsandiego.com (guest-facing), and integration points with third-party booking platforms like Boatsetter. A new charter booking triggered a cascade of infrastructure tasks:

  • Create a shareable guest confirmation page
  • Host it at a predictable, friendly URL path (/g/)
  • Support both legacy booking-ID slugs and human-readable event identifiers
  • Handle static HTML assets and time-aware photo uploads
  • Auto-notify crew with magic links via Lambda-triggered emails

The challenge: sailjada.com and queenofsandiego.com are separate CloudFront distributions fronting different S3 buckets, with different CloudFront function behavior.

Architecture: CloudFront Functions and Flat File Convention

We discovered that queenofsandiego.com's CloudFront distribution includes a custom CloudFront function that rewrites paths. The function intercepts requests to /g/XHQGMDH and maps them to /g/XHQGMDH.html in the origin S3 bucket — allowing us to serve directory-style URLs without maintaining actual directory structures.

This is a deliberate architectural choice: instead of uploading guest pages as /g/XHQGMDH/index.html (requiring Origin Access Control and directory indexing), we upload flat files as /g/XHQGMDH.html. CloudFront's function layer handles the URL rewrite at edge, before S3 ever sees the request.

Why this pattern? It reduces S3 access patterns, eliminates directory listing vulnerabilities, and keeps the origin simple. The rewrite happens in-region at CloudFront, not in the origin.

Technical Implementation

Step 1: Guest Page Generation

When a booking is confirmed, a Lambda function generates a single-page HTML file containing:

  • Charter details (date, time, location, captain, crew)
  • Embedded photo upload widget (time-aware, using presigned POST URLs)
  • Guest checklist and instructions
  • Responsive design for mobile/tablet guests

The file is written to disk temporarily at /tmp/jada-guest-[BOOKING_ID].html, then uploaded directly to S3:

s3://queenofsandiego.com/g/XHQGMDH.html

The booking ID (or event slug) becomes the filename, making the mapping deterministic and reversible.

Step 2: S3 Upload and CloudFront Invalidation

After file generation, we:

  • Upload the HTML file to s3://queenofsandiego.com/g/XHQGMDH.html
  • Set Cache-Control headers to allow short TTLs (60-300 seconds)
  • Invalidate the CloudFront cache for /g/XHQGMDH* to force immediate propagation

CloudFront distribution ID for queenofsandiego.com: E[DISTRIBUTION_ID] (stored in secrets). Invalidations use the AWS SDK or CLI:

aws cloudfront create-invalidation \
  --distribution-id E[DISTRIBUTION_ID] \
  --paths "/g/XHQGMDH*"

The wildcard invalidates both the rewritten path and any variant, ensuring no stale cache.

Step 3: Photo Upload Integration

Guest pages include a time-aware photo upload widget. The backend (SCC Lambda at /g/[EVENT_ID]) generates presigned POST policies that expire after the charter ends. The guest-facing page renders this policy as a form:

  • Form action points to s3://queenofsandiego-photos.s3.amazonaws.com
  • Policy is embedded, not fetched dynamically (reduces cross-origin requests)
  • Photos are stored in a separate S3 bucket to isolate access patterns

The presigned POST includes:

  • expiration: ISO 8601 timestamp, set to 2 hours after charter end
  • content-type: Restricted to image/* MIME types
  • x-amz-meta-* tags: Event ID, guest email, timestamp

Step 4: Crew Notification and Magic Links

When an SCC event is created, a Lambda function queries DynamoDB for crew assignments and generates magic-link URLs. The crew-facing page is served at a different path, also on queenofsandiego.com, but with different access control:

  • Guest page (/g/[EVENT_ID].html): Public, read-only
  • Crew page (/crew/[EVENT_ID]/[MAGIC_LINK]): Magic-link protected, includes checklist and real-time notifications

Magic links are generated as SHA256 hashes of the event ID + crew email + secret salt, stored in a DynamoDB table for verification at request time. This avoids JWT complexity while remaining cryptographically sound.

Infrastructure Changes

S3 Buckets Modified:

  • sailjada.com: Removed guest pages (legacy location)
  • queenofsandiego.com: Added /g/ directory for guest pages
  • queenofsandiego-photos.s3.amazonaws.com: New bucket for photo uploads, separate for access isolation

CloudFront Distributions:

  • queenofsandiego.com distribution: Verified CloudFront function includes path rewriting logic
  • Origin: s3://queenofsandiego.com.s3.us-west-2.amazonaws.com
  • Function association: Viewer request (intercepts /g/* paths)

Lambda Functions Updated:

  • scc-lambda-src/lambda_function.py: Added guest page generation and presigned POST creation
  • New route handler: POST /events triggers guest page generation as side effect
  • Existing route: