Multi-Domain Event Management: Automating Charter Booking Workflows Across Sailjada, ShipCaptainCrew, and Guest Pages
This post details the technical implementation of a unified charter booking workflow that automates event creation, crew notification, guest page generation, and financial tracking across three interconnected systems: a JADA Internal Calendar (Google Calendar API), ShipCaptainCrew (SCC) Lambda-based backend with DynamoDB), and guest-facing pages hosted on CloudFront + S3. The challenge: coordinate five separate write operations across different authentication boundaries, S3 distributions, and API gateways—all triggered from a single user action.
What Was Done
For a single Boatsetter charter booking, we automated the creation of:
- A JADA Internal Calendar event (Google Calendar API, service account auth)
- An SCC event in DynamoDB with crew notification (Lambda service key auth)
- A guest-facing page at
/g/SLUGuploaded to CloudFront S3 origin - A crew-facing page with checklist and logistics (HTML generated server-side, served via SCC frontend)
- Email notifications to all assigned crew with magic links
All of this completed in parallel, with proper error handling and idempotency guarantees.
Technical Details
Authentication Layer Complexity
The biggest hurdle: each system uses different authentication mechanisms, and CloudFront strips custom headers at the distribution boundary.
- JADA Calendar (Google Workspace): Service account with private key stored in `/Users/cb/.claude/projects/memory/` project secrets. Auth header:
Authorization: Bearer {JWT_TOKEN}(generated on-the-fly from key material). - SCC Lambda (API Gateway): Custom header validation. Lambda reads
X-Service-Key, hashes it withhashlib.sha256(key.encode()).hexdigest(), and compares against environment variableSERVICE_KEY_HASH. - CloudFront + S3: No authentication for public guest pages; instead, we use path rewriting via CloudFront Functions to map friendly URLs to flat .html files per S3 naming conventions.
Initial attempts to call SCC via CloudFront distribution URL failed because CF strips the X-Service-Key header. Solution: bypass CloudFront and hit the API Gateway URL directly (https://{API_ID}.execute-api.{region}.amazonaws.com/prod/events), which doesn't strip headers.
S3 and CloudFront Distribution Strategy
Two separate S3 + CloudFront stacks for different purposes:
- sailjada.com: S3 bucket `sailjada.com`, CloudFront distribution ID `E{DIST_ID_1}`. Used for internal tools, dashboards, and backend services.
- queenofsandiego.com: S3 bucket `queenofsandiego.com`, CloudFront distribution ID `E{DIST_ID_2}`. Public-facing guest pages and crew pages. This distribution has a CloudFront Function attached that rewrites paths: requests to `/g/{slug}` are rewritten internally to `/g/{slug}.html` to match flat file naming in S3.
The CloudFront Function (read-only in this session) handles the rewrite logic. We upload guest pages as `/g/{slug}.html` to S3 at s3://queenofsandiego.com/g/{slug}.html, then immediately invalidate the CloudFront cache with:
aws cloudfront create-invalidation \
--distribution-id E{DIST_ID_2} \
--paths '/g/{slug}*'
This ensures the new page is live within seconds.
Event Creation Workflow (Parallel Operations)
All four operations run concurrently, with fallback error handling:
- JADA Calendar Event: Call Google Calendar API with event details (date, time, charter notes). Stores metadata for internal crew scheduling.
- SCC Event (DynamoDB): POST to
/eventsendpoint with crew IDs, dates, and notes. Lambda validates the service key, writes to DynamoDB table `events`, and triggers SNS topic that sends email notifications to all assigned crew. - Guest Page: Generate static HTML with charter details, photo upload endpoint, and crew logistics. Save as `/tmp/{slug}.html`, upload to S3, invalidate CF.
- Email Notifications: SCC Lambda automatically sends crew notifications via SES with magic link to crew-facing page. (This is included in the SCC event creation flow.)
Pseudo-code structure:
async def create_charter_booking(boatsetter_data):
tasks = [
create_calendar_event(boatsetter_data),
create_scc_event(boatsetter_data),
generate_and_upload_guest_page(boatsetter_data),
]
results = await asyncio.gather(*tasks, return_exceptions=True)
return results
DynamoDB Direct Updates for Sensitive Data Removal
After SCC event creation, we needed to scrub revenue details from the event notes so crew cannot see how much the charter earned. Rather than making another API call (which could fail or be slow), we directly updated the DynamoDB item:
- Table: `scc-events`
- Key: `event_id` (partition key)
- Remove attributes: `revenue`, `captain_fee` from the `notes` JSON field
This approach is faster and avoids additional Lambda invocations. The Lambda function in `/tmp/scc-lambda-src/lambda_function.py` contains the event routing logic; we inspected the `handle_event_update()` function to understand the update schema, then used boto3 directly to patch the item.
Guest Page Architecture
The guest page is a single static HTML file with embedded JavaScript for photo uploads. Key features:
- Photo Upload Endpoint: Guest-facing form POSTs to
/g/{event_id}/photos, which is handled by SCC Lambda. The handler (`handle_guest_presign()`) generates a pre-signed S3 URL for the guest to upload directly to S3, avoiding bandwidth through Lambda. - Time-Aware Uploads: Upload form is disabled after charter completion time. JavaScript reads the event's `end_time` and disables the form client-side; server-side validation in Lambda also rejects uploads after the cutoff.
- Crew Checklist Integration: Embedded checklist shows crew tasks (e.g., "Stock cooler," "Check fuel," "Inspect sails"). This is generated from the SCC event's `crew_tasks` array and is read-only on the guest page (only crew can edit via their crew-facing dashboard).
Key Decisions and Trade-Offs
Why bypass CloudFront for SCC API calls? CloudFront strips custom headers by default for security. We could have configured CloudFront to allow `X-Service-Key`, but that's risky—it could inadvertently expose the key in logs. Instead, we call the API Gateway URL directly, which preserves headers. This adds slight latency (no CF caching), but API calls are infrequent and latency-tolerant.