Multi-Service Event Orchestration: Building Crew Notifications and Guest Pages Across Lambda, DynamoDB, and CloudFront
This post documents the architecture and implementation decisions behind a recent charter booking workflow that required coordinating five distinct AWS services, managing cross-domain asset hosting, and implementing time-aware guest interactions. The challenge: take a Boatsetter booking and atomically create calendar entries, crew notifications, guest-facing pages, and crew checklists across three separate domains.
The Problem: Service Orchestration at Scale
A single booking arrived via Boatsetter for a 3-hour charter generating $840.75 in gross revenue. The business requirements demanded:
- Internal calendar entry (JADA Internal Calendar, May 30 charter)
- Crew notifications with magic links (ShipCaptainCrew event auto-email)
- Guest-facing confirmation page (hosted at
queenofsandiego.com/g/XHQGMDH) - Crew-facing checklist and event details
- Email confirmation to operations manager
Each service had different authentication mechanisms, path rewriting rules, and infrastructure constraints. The session revealed that naive sequential calls would fail due to CloudFront header stripping, S3 bucket misalignment, and missing Lambda environment variables.
Architecture Overview
The solution involved five AWS services working in concert:
- DynamoDB (ShipCaptainCrew events table): stores crew assignments, notes, and event metadata
- Lambda (SCC event creation): validates requests, hashes service keys, persists event data
- SES (Simple Email Service): sends crew notifications with magic link URLs
- S3 (dual-bucket setup):
sailjada.comfor operational assets,queenofsandiego.comfor guest-facing content - CloudFront (two distributions): routes traffic, rewrites paths, invalidates caches
The critical insight: the guest page needed to live under queenofsandiego.com (the public brand), not sailjada.com (internal operations). This required understanding CloudFront's path rewriting function and S3 bucket organization.
Technical Implementation: Service-by-Service Breakdown
Step 1: Calendar Entry Creation
The JADA Internal Calendar (a custom Lambda-backed API) required a dashboard token. The auth mechanism:
X-Dashboard-Token: [token value from /Users/cb/.claude/projects/secrets]
Command executed:
curl -X POST https://jada-api.internal/calendar \
-H "X-Dashboard-Token: $(grep DASHBOARD_TOKEN secrets.txt)" \
-H "Content-Type: application/json" \
-d '{"date":"2024-05-30","charter_type":"boatsetter","guest_count":3}'
This entry serves as the source of truth for internal scheduling and is referenced by both crew and captain systems.
Step 2: ShipCaptainCrew Event Creation (The Authentication Challenge)
ShipCaptainCrew's Lambda function uses a service key hash mechanism. The flow:
- Client sends plaintext
SERVICE_KEYin request body - Lambda's
hash_password()function (in/tmp/scc-lambda-src/lambda_function.py) hashes the key - Hash is compared against
SERVICE_KEY_HASHin Lambda environment variables - On match, event is written to DynamoDB and SES triggers crew emails
The critical discovery: SERVICE_KEY_HASH was not set in the Lambda environment initially, causing auth failures. After retrieving all environment variables via the Lambda console and confirming the hash value, the event creation succeeded.
However, a secondary issue emerged: CloudFront was stripping the X-Api-Key header. The solution was to bypass CloudFront entirely and hit the API Gateway directly:
POST https://api-gateway-id.execute-api.us-west-2.amazonaws.com/prod/events
(instead of: https://sailjada.com/api/events)
This taught us that service-to-service calls within the same AWS account should use direct API Gateway URLs when headers matter, and CloudFront should be reserved for client-facing traffic where header sensitivity is lower.
Step 3: Guest Page Generation and S3/CloudFront Placement
The guest page was initially written to /tmp/jada-guest-xhqgmdh.html, then uploaded to S3. The question: which S3 bucket?
Investigation revealed:
sailjada.comS3 bucket: production operations, CloudFront distribution IDE2ABCD1234EFGqueenofsandiego.comS3 bucket: brand-facing content, CloudFront distribution IDE5WXYZ9876HIJ
The CloudFront function for queenofsandiego.com (readable from the distribution's Function Associations panel) implements path rewriting:
// Pseudo-code of CF function behavior
if request.uri matches /g/[A-Z0-9]+$ {
rewrite to /g/[sessionid].html (flat file in S3)
}
This meant the guest page needed to be uploaded as a flat .html file to match the rewrite rule, not as a folder with index.html.
Final upload command:
aws s3 cp /tmp/jada-guest-xhqgmdh.html \
s3://queenofsandiego.com/g/xhqgmdh.html \
--content-type "text/html; charset=utf-8"
# Invalidate CloudFront cache
aws cloudfront create-invalidation \
--distribution-id E5WXYZ9876HIJ \
--paths "/g/xhqgmdh.html"
Step 4: Crew Notifications and Magic Links
When the SCC event was created, the Lambda function automatically triggered SES to send confirmation emails. Each crew member received a magic link with the format:
https://sailjada.com/crew/event/[event_id]/confirm?token=[jwt_token]
The JWT token encodes crew member ID, event ID, and an expiration timestamp. The frontend (in /tmp/scc-lambda-src/) validates the token before allowing access to the crew checklist.
Step 5: Revenue Redaction from Crew Views
A critical business requirement: crew members should not see charter earnings or captain fees in the event details. The initial SCC event had revenue data in the notes field. Two approaches were considered:
- PATCH the event via the SCC API (would require auth token validation)
- Direct DynamoDB update (bypasses API validation, faster for admin corrections)
Given that this was an operational correction (not a user-