```html

Automating Charter Booking Workflows: Multi-Service Orchestration Between CloudFront, Lambda, DynamoDB, and S3

What Was Done

We implemented an end-to-end charter booking automation system that provisions a guest-facing page, crew coordination event, internal calendar entry, and crew notification—all triggered by a single booking. The system spans five AWS services (CloudFront, Lambda, DynamoDB, S3, SES), two custom web applications, and requires careful orchestration of authentication across service boundaries.

The Problem

Manual charter workflows were fragmented: booking confirmation required separately creating calendar entries, notifying crew via email, provisioning a guest page, and logging into multiple dashboards. Each step was error-prone and time-consuming. The goal was to automate the entire chain with a single trigger while respecting service architecture constraints—particularly CloudFront header stripping and Lambda cold-start latency.

Architecture Overview

The system uses a hub-and-spoke pattern with these components:

  • Guest Page Generator: Builds a dynamic HTML page at CloudFront path /g/{booking-slug} served from S3 bucket queenofsandiego.com
  • ShipCaptainCrew (SCC) Event API: Creates crew coordination events via Lambda, which auto-triggers Cognito magic-link emails to all crew
  • JADA Internal Calendar: Persists booking metadata in a separate Lambda-backed calendar system
  • Crew Notification: SES-based email summarizing the booking, sent to crew with confirmation links

Technical Implementation

Guest Page Provisioning

Guest pages are stored in S3 at s3://queenofsandiego.com/g/{booking-slug}.html. The CloudFront distribution (ID: E1ABCD1234...) uses a custom function to rewrite /g/{slug} requests to /g/{slug}.html before hitting the S3 origin.

The page is a single self-contained HTML file containing:

  • Booking details (date, time, duration, guest count)
  • Photo upload widget that presigns S3 URLs via the SCC Lambda's POST /g/presign route
  • Inline CSS and JavaScript (no external dependencies to minimize latency)
  • Time-aware upload restriction (disables uploads after charter end time)

The upload presign endpoint requires AWS Signature Version 4 authentication. The guest page constructs the request body with:

{
  "booking_id": "xhqgmdh",
  "file_name": "photo.jpg",
  "file_size": 2048576,
  "mime_type": "image/jpeg"
}

This hits https://api.shipcrewcaptain.com/g/presign (direct API Gateway URL, bypassing CloudFront header stripping) and returns a presigned S3 POST policy.

SCC Event Creation

The ShipCaptainCrew Lambda at arn:aws:lambda:us-west-2:ACCOUNT:function:ShipCaptainCrew handles event creation via POST /events.

This endpoint required authentication via X-Service-Key header. The Lambda checks this header against SERVICE_KEY_HASH in its environment—but the hash wasn't initially set, causing requests to fail silently. We resolved this by:

  1. Downloading the Lambda source from CloudWatch Logs' downloadable zip
  2. Inspecting handlers/events.py to find the hash comparison logic
  3. Retrieving the actual service key from AWS Secrets Manager
  4. Computing the hash locally: hashlib.sha256(service_key.encode()).hexdigest()
  5. Adding the hash to Lambda environment variables via the console

Once authenticated, the endpoint creates a DynamoDB entry in table ShipCaptainCrew-Events-Prod and triggers SNS notifications to all crew. The SNS topic integrates with Cognito to auto-generate magic-link emails—no manual email construction needed.

Critical: we explicitly removed revenue and captain fee from the event notes before creation. Crew should not see earnings. We updated the DynamoDB item directly via the console's query interface to strip sensitive fields.

Calendar Entry Creation

The JADA Internal Calendar is a separate Lambda backend at arn:aws:lambda:us-west-2:ACCOUNT:function:jada-calendar. It requires X-Dashboard-Token header instead of service key.

The token is stored in Secrets Manager and rotated via a separate process. The request body structure:

{
  "booking_id": "xhqgmdh",
  "charter_date": "2024-05-30",
  "charter_start": "10:00",
  "charter_end": "13:00",
  "guest_count": 4,
  "source": "boatsetter"
}

This Lambda writes to DynamoDB table JadaCalendar-Prod and also triggers an update to a static JSON file in S3 bucket sailjada.com at key calendar/2024.json, invalidating CloudFront distribution E2EFGH5678....

Crew Notification Email

We use SES (Simple Email Service) in sandbox mode, configured for the region where the Lambda runs. The email is sent to the crew distribution list with:

  • Charter details (date, time, location, guest count)
  • Confirmation link (magic link from SCC event creation)
  • Link to crew-facing page (served from SCC at /crew/{event_id})
  • BCC to the captain for record-keeping

SES is preferred over SNS because we needed custom HTML formatting and fine-grained control over headers. The email is sent after both SCC and calendar entries succeed.

Key Infrastructure Decisions

Why Direct API Gateway URLs for SCC?

The guest page needs to hit the presign endpoint from the browser. CloudFront distribution E1ABCD1234... strips custom headers (including auth tokens) per AWS security defaults. We worked around this by using the direct API Gateway URL api.shipcrewcaptain.com, which bypasses CloudFront entirely. This is registered in Route53 as an alias record pointing to the API Gateway regional endpoint.

Why Separate Calendar System?

The calendar Lambda uses different authentication (dashboard token vs. service key) because it's accessed by the booking admin interface, which needs different identity semantics than crew-facing APIs. Separating these prevents token scope creep and makes rotation easier.

Why DynamoDB Direct Updates?

When we needed to remove revenue from SCC events post-creation, we couldn't use the event update endpoint (which would have been the RESTful approach) because it wasn't in the Lambda's routing table. Direct DynamoDB updates via the console were faster than deploying a new Lambda route, but this is a technical debt item—the SCC Lambda should expose a PATCH endpoint.

Observability and Debugging