Multi-Domain Guest Page Architecture: Routing, CloudFront Functions, and S3 Path Conventions
This post documents the infrastructure pattern we implemented to support dynamic guest-facing charter pages across multiple domains, with emphasis on CloudFront function routing, S3 object naming conventions, and authentication patterns for crew-facing operations.
What Was Built
We created a unified guest page system that:
- Generates dynamic HTML guest pages for charter bookings
- Serves pages from
queenofsandiego.com/g/{booking-slug}via CloudFront function path rewriting - Integrates with S3 bucket
queenofsandiego.comusing flat HTML file storage (no directory nesting) - Handles photo uploads with time-aware presigned URLs from ShipCaptainCrew Lambda
- Creates corresponding internal calendar entries and crew notifications via separate infrastructure
- Routes authentication through API Gateway to bypass CloudFront header stripping
Technical Architecture
CloudFront Function Path Rewriting
The core routing logic lives in a CloudFront function attached to queenofsandiego.com distribution. This function intercepts requests to /g/* paths and rewrites them to S3 object keys.
Why this approach: CloudFront functions execute at edge with sub-millisecond latency and zero additional cost. This allows us to maintain a clean public URL structure (/g/friendly-booking-slug) while storing objects as flat filenames in S3 (jada-guest-xhqgmdh.html).
The function reads the incoming request path, extracts the booking identifier, and rewrites the origin request to fetch the corresponding S3 object:
// CloudFront function pattern (pseudocode)
// Request: GET /g/my-booking-name
// Rewritten to: GET /jada-guest-xhqgmdh.html
// Origin: S3 bucket queenofsandiego.com
S3 Storage Convention
Guest pages are stored as flat HTML files in the bucket root, using session-based filenames:
- File path:
s3://queenofsandiego.com/jada-guest-xhqgmdh.html - Public URL:
https://queenofsandiego.com/g/my-charter-may-30 - Slug mapping: maintained in metadata or database (not implemented in this iteration)
Why flat files: The CloudFront function pattern requires knowing the exact S3 key before making the origin request. Flat naming (one directory level) simplifies the rewrite logic and avoids the need for index.html routing or directory listing queries.
HTML Page Generation
Guest pages are generated dynamically with embedded charter details:
# Generated file: /tmp/jada-guest-xhqgmdh.html
# Contains:
# - Charter date, duration, location
# - Guest-facing checklist and instructions
# - Form for photo uploads (presigned URL endpoint)
# - Styling inline or via <style> tags
# - No external dependencies (self-contained)
The page includes a photo upload form that targets a presigned URL endpoint. This endpoint is generated by the ShipCaptainCrew Lambda function at /g/{booking-id}/presign, which validates the request and returns a time-limited S3 PUT URL.
Authentication and API Integration
Dashboard Lambda Authentication
Internal operations (calendar creation, crew notifications) require authentication to the dashboard Lambda. This Lambda expects the header:
X-Dashboard-Token: {token-value}
The token is verified server-side by comparing a hash with the Lambda environment variable DASHBOARD_TOKEN_HASH.
ShipCaptainCrew API Gateway Direct Access
When CloudFront distributions strip authentication headers (a common security practice for caching layers), we bypass the CloudFront distribution and hit the API Gateway endpoint directly:
- CloudFront URL:
https://scc.internal.sailjada.com/events(headers stripped) - API Gateway URL:
https://api-gateway-id.execute-api.us-west-2.amazonaws.com/prod/events(headers preserved)
Why two routes: CloudFront provides caching and edge delivery for guest-facing content. API Gateway provides direct access for authenticated backend operations. We use the appropriate endpoint based on whether caching is needed and whether custom headers must be preserved.
Service Key Hashing
The ShipCaptainCrew Lambda validates API requests using a service key. The key is hashed before storage in Lambda environment variables:
# Environment variable in Lambda:
SERVICE_KEY_HASH=sha256_hash_of_actual_service_key
# Request includes:
X-Service-Key: {unhashed-key}
# Lambda validates by:
hash(request_header) == SERVICE_KEY_HASH
Why hashing: Credentials in environment variables are visible in Lambda console and CloudWatch logs. Hashing ensures the actual key is never exposed, even if logs are compromised.
Infrastructure Details
S3 Bucket Configuration
Bucket: queenofsandiego.com
Region: us-west-2
Versioning: Enabled (for guest page rollback)
Block Public Access: Enabled (CloudFront only)
CORS: Configured for presigned URL uploads
Lifecycle: 90-day expiration on guest pages
CloudFront Distribution
Domain: queenofsandiego.com
Origin: S3 bucket queenofsandiego.com
Function Association: Viewer Request (path rewriting)
Cache TTL: 3600s (1 hour) for guest pages
Invalidation: Manual trigger on page updates
Workflow Automation
Creating a new charter booking triggers a sequence:
- Generate guest HTML page
- Upload to S3:
s3://queenofsandiego.com/jada-guest-{session-id}.html - Invalidate CloudFront:
/jada-guest-{session-id}.html - Create JADA Internal Calendar entry (via dashboard Lambda with auth token)
- Create ShipCaptainCrew event (via API Gateway with service key)
- Event creation auto-triggers crew notifications with magic links
- Send email summary to operations team via SES
Key Decisions and Rationale
Single Domain vs. Multi-Domain
Guest pages are now served from queenofsandiego.com instead of sailjada.com. This reflects the brand identity of the vessel and improves user experience. Internal infrastructure still uses sailjada.com for admin tools.
Flat File Storage
Rather than organizing guest pages in directories (/guests/2024-05/xhqgmdh.html