Building a Multi-Domain Charter Booking Workflow: CloudFront, Lambda, DynamoDB, and S3 Integration
What Was Done
We built an end-to-end charter booking workflow that spans two separate domain properties (sailjada.com and queenofsandiego.com), integrates with a third-party booking platform (Boatsetter), and automatically provisions infrastructure for crew coordination. The system creates calendar entries, generates guest-facing booking pages with time-aware photo uploads, provisions SCC (ShipCaptainCrew) events with crew notifications, and maintains financial tracking—all from a single booking trigger.
Technical Details: The Booking Pipeline
Infrastructure Stack
- Primary booking system: ShipCaptainCrew (SCC) Lambda running on API Gateway (v1)
- Calendar system: JADA Internal Calendar Lambda (separate API, token-authenticated)
- Content delivery: CloudFront distributions for sailjada.com and queenofsandiego.com, both backed by S3
- Data persistence: DynamoDB tables for SCC events, guest pages, and crew assignments
- Email: AWS SES for crew notifications and guest confirmations
Guest Page Generation and Deployment
Guest pages are static HTML artifacts generated per booking, stored in S3, and served through CloudFront with path-rewriting rules. The deployment pattern works like this:
- Guest page HTML is written to
/tmp/jada-guest-[BOOKING_ID].htmlduring booking creation - Uploaded to
s3://queenofsandiego.com/g/[FRIENDLY_SLUG].html(not the S3 website endpoint, the bucket directly) - CloudFront function intercepts requests to
/g/*and rewrites them to/g/[SLUG].html, then caches aggressively - After upload, CloudFront cache is invalidated with
aws cloudfront create-invalidation --distribution-id [DIST_ID] --paths "/g/*"
The reason for this pattern: S3 website mode doesn't support HTTPS or custom domains well, and CloudFront's origin access control (OAC) prevents direct S3 website endpoint access. Instead, we treat S3 as a pure origin and use Lambda@Edge (or CloudFront Functions for non-regex patterns) to handle path rewriting. This keeps routing logic version-controlled and avoids S3 Static Website Hosting.
Cross-Domain Coordination
The booking references must exist in multiple systems:
- SCC Event: Created via
POST /api/eventsto the SCC Lambda API Gateway endpoint. This auto-generates magic links for crew and sends notifications via the SCC frontend's email template system. - JADA Calendar Entry: Created via a separate Lambda with token-based auth (header:
X-Dashboard-Token). This endpoint is internal and used by dashboard scripts to track revenue, captain assignments, and internal bookings. - Guest Page: Deployed to queenofsandiego.com via S3 + CloudFront, keyed by a friendly URL slug (e.g.,
/g/may-30-boatsetter-charter/instead of a booking UUID).
Authentication and Security Concerns
A critical issue emerged during implementation: the SCC API Gateway endpoint is behind CloudFront, and CloudFront strips custom headers by default. When calling the SCC Lambda with X-API-Key: [SERVICE_KEY], the header was stripped before reaching Lambda. The solution:
- Identify the underlying API Gateway endpoint (found in CloudFront origin settings)
- Call the API Gateway directly instead of through CloudFront:
https://[API_ID].execute-api.[REGION].amazonaws.com/prod/events - Configure CloudFront to allowlist custom headers if public traffic also needs to pass them: add
X-API-Keyto the origin request policy (though this wasn't necessary for internal Lambda-to-Lambda calls)
The SCC Lambda validates the service key by hashing it and comparing to an environment variable SERVICE_KEY_HASH. The hash function is defined in the Lambda code (not exposed here for security), and the hash must be pre-computed and set in the Lambda environment during infrastructure-as-code deployment.
Financial Tracking and Sensitive Data Removal
A subtle but important detail: the SCC event notes initially included revenue and captain fee information. Crew members can see event notes via the SCC frontend, so this data was leaking to crew. The fix required direct DynamoDB access because the SCC event update Lambda endpoint doesn't support selective field removal.
Process:
- Query the SCC DynamoDB table (table name found in Lambda environment variables) with the event ID
- Update the item to remove the
notesfield or sanitize it before crew visibility - This bypasses the Lambda layer entirely, ensuring crew-facing data doesn't contain financial details
Key Architectural Decisions
Why Separate CloudFront Distributions?
sailjada.com and queenofsandiego.com are separate CloudFront distributions because they serve different purposes: sailjada is the internal crew-facing platform, while queenofsandiego is the public guest and marketing site. Keeping them separate allows independent cache invalidation, different origin request policies, and cleaner access logs.
Why Guest Pages Are Static HTML, Not Lambda-Generated?
Guest pages could theoretically be generated on-the-fly by a CloudFront origin Lambda. However, static HTML files are preferable because:
- No cold start latency
- Deterministic caching behavior (S3 ETags don't change if content doesn't change)
- Easy to preview during development (just open the .html file in a browser)
- Trivial to version control guest page templates separately from Lambda code
Token-Based Auth for Internal Lambdas
The JADA Calendar Lambda uses X-Dashboard-Token rather than API keys because it's internal-only, called from specific scripts (e.g., /tmp/update_dashboard.py). The token is stored in a secrets manager and rotated separately from SCC service keys. This compartmentalization means a leaked SCC key doesn't compromise calendar endpoints.
Deployment Commands and Infrastructure Changes
# Example: Upload guest page and invalidate cache
aws s3 cp /tmp/jada-guest-xhqgmdh.html \
s3://queenofsandiego.com/g/may-30-boatsetter-charter.html \
--content-type "text/html" \
--cache-control "max-age=3600"
# Invalidate CloudFront to force immediate cache refresh
aws cloudfront create-invalidation \
--distribution-id [QUEENOFSANDIEGO_DIST_ID] \
--paths "/g/*"
# Create SCC event via direct API Gateway (bypasses CloudFront header stripping)
curl -X POST https://[SCC_API_ID].execute-api.[REGION].amazonaws.com/prod/events \
-H "Content-Type: application/