Multi-Domain Guest Experience Architecture: Decoupling Charter Pages from Booking Events
This post walks through a complete infrastructure refactor for handling guest-facing charter pages across multiple domains, automating crew notifications, and centralizing booking state management. The session involved coordinating three separate services (SailJada calendar, ShipCaptainCrew events, and domain-specific S3/CloudFront distributions) into a cohesive booking workflow.
The Problem
Previously, guest pages were tightly coupled to individual booking IDs and lived on a single domain. This created several issues:
- Guest pages had to be rebuilt and redeployed to S3 whenever booking details changed
- No single source of truth for crew assignments, captain fees, or port costs
- Crew notifications were manual or nonexistent
- Adding new domains (like
queenofsandiego.com) required duplicating the entire hosting infrastructure - Revenue data exposed to crew members through event notes
Architecture Overview
The solution decouples three concerns:
- Booking State Management: ShipCaptainCrew Lambda handles all event data (crew assignments, timing, costs)
- Calendar/Scheduling: JADA Internal Calendar Lambda tracks internal booking workflow
- Guest Experience: Static HTML pages per domain, dynamically populated with read-only booking data via presigned URLs
The workflow now looks like:
Booking Created → SCC Event Created (auto-notifies crew)
↓
JADA Calendar Entry
↓
Generate Guest Page (domain-specific S3 bucket)
↓
Crew receives magic link with confirmation checklist
Technical Implementation
1. Service Key Authentication for SCC Lambda
ShipCaptainCrew protects its Lambda endpoints with a service key hash. The flow:
- Service key stored in secrets manager:
/sailjada/shipcaptaincrew/SERVICE_KEY - SCC Lambda environment includes
SERVICE_KEY_HASH(salted hash of the service key) - Incoming requests are authenticated by hashing the
X-Service-Keyheader and comparing
The hash function in /tmp/scc-lambda-src/lambda_function.py:
def hash_password(password):
salt = os.environ.get('PASSWORD_SALT', 'default_salt')
return hashlib.sha256((password + salt).encode()).hexdigest()
When calling SCC endpoints, include:
curl -X POST https://api.shipcaptaincrew.example/events \
-H "X-Service-Key: YOUR_SERVICE_KEY" \
-H "Content-Type: application/json" \
-d '{...event_payload...}'
CloudFront strips custom headers by default, so we hit the API Gateway directly at the regional endpoint (bypassing CF) when creating events.
2. Guest Page Routing with CloudFront Functions
The queenofsandiego.com CloudFront distribution uses a CloudFront Function to rewrite guest page paths. The function (stored in /tmp/scc-crew-invite-guide.html structure) maps:
/g/BOOKING_ID→ points to/g/BOOKING_ID.htmlin the S3 bucket/g/friendly-charter-name→ same mechanism, different slug
This allows flat .html files in S3 to be served at clean URLs without directory structures. The CF function:
if (request.uri.startsWith('/g/')) {
request.uri = request.uri + '.html';
}
S3 bucket: queenofsandiego.com, CloudFront distribution ID: E3EXAMPLE12345
3. Dashboard Lambda Authentication
The dashboard Lambda (used for calendar entries and progress tracking) requires X-Dashboard-Token header. This is separate from SCC service key auth:
curl -X POST https://dashboard-api.example/calendar \
-H "X-Dashboard-Token: YOUR_DASHBOARD_TOKEN" \
-H "Content-Type: application/json" \
-d '{...calendar_entry...}'
This token is sourced from /sailjada/dashboard/AUTH_TOKEN in secrets.
4. Crew Notification Automation
When an SCC event is created with crew assignments, the Lambda automatically:
- Generates magic links (time-limited presigned URLs with crew ID in query string)
- Sends email via SES to all assigned crew members
- Email includes confirmation link and checklist preview
- No manual email sending required
The event payload includes crew assignments in the crew_assignments array, each with role (crew, captain, hostess) and hourly rate.
Infrastructure Changes
S3 Buckets
sailjada.com(primary booking site) — contains main templatesqueenofsandiego.com(brand-specific domain) — contains guest pages for chartered events- Both are origin buckets for respective CloudFront distributions
CloudFront Distributions
sailjada.comdistribution: standard S3 origin, caches HTML files with 5-minute TTLqueenofsandiego.comdistribution: includes CloudFront Function for path rewriting, cache invalidation required after guest page uploads
To invalidate CloudFront after uploading a guest page:
aws cloudfront create-invalidation \
--distribution-id E3EXAMPLE12345 \
--paths "/g/*"
Lambda Functions
scc-lambda-src/lambda_function.py— ShipCaptainCrew event management, presigned URL generation, crew auth- Dashboard Lambda (endpoint: regional API Gateway URL) — calendar and progress tracking
Both are deployed via CloudFormation or SAM, with environment variables injected from secrets manager.
Guest Page Generation
Guest pages are static HTML files uploaded to S3 with a flat naming convention: /g/BOOKING_ID.html or /g/friendly-name.html.
Key design decision: no dynamic templating on the front end. All crew assignments, timing, and pricing are embedded as JSON in a <script> tag during page generation. This keeps the page immutable and cacheable, while still allowing crew to confirm and upload photos via presigned URLs.
Photo uploads are handled by a presigned URL endpoint in SCC Lambda at /g/presign, which requires the