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 endcontent-type: Restricted toimage/*MIME typesx-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 pagesqueenofsandiego-photos.s3.amazonaws.com: New bucket for photo uploads, separate for access isolation
CloudFront Distributions:
queenofsandiego.comdistribution: 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 /eventstriggers guest page generation as side effect - Existing route: