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 toqueenofsandiego.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-Tokenheader (calendar operations) - SCC Lambda: Protected by
X-Service-Keyheader 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