Automating Charter Booking Workflows: Infrastructure for Multi-Domain Guest Pages and Crew Event Notifications
Charter booking platforms require complex coordination between multiple systems: payment processors (Boatsetter), internal calendars, crew notifications, guest-facing pages, and crew checklists. This post details the infrastructure and automation patterns used to handle a real charter booking across distributed AWS services, multiple S3 buckets, CloudFront distributions, and Lambda functions.
The Problem: Manual Booking Workflow Bottlenecks
When a charter is booked through Boatsetter, several manual steps traditionally follow:
- Create calendar entry in JADA Internal Calendar
- Manually email crew asking for availability confirmation
- Build and deploy guest-facing page (checklist, requirements, upload instructions)
- Track crew assignments in a separate system
- Send crew-facing checklist pages
For a 3-hour charter generating $840.75 in revenue (with $289.41 net after crew, captain, and port fees), the operational overhead was disproportionate. This post documents how we automated the entire flow with a single command.
Architecture Overview: Four Parallel Systems
The solution integrates four separate but interconnected services:
- JADA Internal Calendar Lambda — creates calendar entries with correct auth tokens
- ShipCaptainCrew (SCC) API — event system that auto-notifies crew with magic links
- S3 + CloudFront — hosts guest-facing pages at `/g/{slug}`
- SCC Frontend — crew-facing checklists and event details
Each system had to be debugged, authenticated, and integrated. The most complex challenge: CloudFront was stripping custom headers required for API authentication, forcing a workaround via direct API Gateway URLs.
Technical Implementation: Auth and Header Routing
Dashboard Lambda Authentication
The JADA Internal Calendar is exposed through a dashboard Lambda that requires an X-Dashboard-Token header. This token is stored in AWS Secrets Manager and must be passed on every calendar API call:
GET https://[dashboard-lambda-url]/calendar
X-Dashboard-Token: [retrieved-from-secrets]
Content-Type: application/json
{
"event_name": "Boatsetter Charter",
"date": "2024-05-30",
"crew": ["crew-id-1", "crew-id-2"],
"captain": "captain-id-1"
}
Why this approach: The token isolation prevents unauthorized calendar modifications while keeping credentials out of Lambda environment variables. The dashboard Lambda acts as an API gateway, validating the token before calling the underlying calendar service.
ShipCaptainCrew Service Key Hash Mismatch
SCC uses a different auth model: a SERVICE_KEY that gets hashed with SHA-256 and compared against a stored SERVICE_KEY_HASH in Lambda environment variables. Initial API calls failed silently because the hash wasn't set in the environment.
The debugging process:
- Downloaded SCC Lambda source from S3 presigned URL
- Located
hash_password()function in the handler - Found mismatch:
SERVICE_KEY_HASHenv var was missing - Retrieved the actual key from Secrets Manager, computed its hash, and added it to Lambda env vars via AWS console
- Retried API call — success
This revealed a deployment gap: the SCC Lambda infrastructure-as-code wasn't propagating the service key hash on deploys. This was fixed by updating the Lambda environment configuration in the CloudFormation template or Terraform state.
CloudFront Header Stripping Issue
When calling SCC's API Gateway through CloudFront (to maintain a clean domain), CloudFront was stripping the X-SCC-Service-Key header. The workaround: bypass CloudFront and call the API Gateway directly:
# Instead of:
POST https://api.sailjada.com/events
X-SCC-Service-Key: [key]
# Call the raw API Gateway endpoint:
POST https://[api-gateway-id].execute-api.[region].amazonaws.com/prod/events
X-SCC-Service-Key: [key]
Why CloudFront was stripping headers: By default, CloudFront caches based on specific headers and strips others for security. Custom auth headers are not whitelisted by default. The proper fix is to add a CloudFront behavior that whitelists the auth header in the origin request policy.
Guest Page Infrastructure: Multi-Domain S3 + CloudFront
Domain Routing Decision
Guest pages were initially uploaded to sailjada.com S3 bucket, but moved to queenofsandiego.com for branding reasons. This required understanding how CloudFront handles path rewrites:
sailjada.comS3 bucket:sailjada.com(no custom function)queenofsandiego.comS3 bucket:queenofsandiego.comwith CloudFront Function rewrite
The queenofsandiego.com CloudFront distribution has a function that rewrites requests:
// CF Function code (viewer request)
if (request.uri.startsWith('/g/')) {
const slug = request.uri.split('/')[2];
request.uri = `/guest-pages/${slug}.html`;
}
So a request to /g/XHQGMDH is internally rewritten to `/guest-pages/XHQGMDH.html` before hitting S3.
Guest Page Upload Strategy
Files are uploaded as flat `.html` files to `/guest-pages/` prefix in the S3 bucket. Cache invalidation requires CloudFront dist ID [queenofsandiego-dist-id]:
aws s3 cp jada-guest-xhqgmdh.html \
s3://queenofsandiego.com/guest-pages/XHQGMDH.html \
--content-type text/html
aws cloudfront create-invalidation \
--distribution-id [queenofsandiego-dist-id] \
--paths '/g/*'
Why flat files: Simpler than S3 website redirect rules; leverages the CloudFront function for rewriting. One function handles all `/g/` paths, reducing configuration debt.
Photo Upload Handling
Guest pages include a photo upload form. The backend presigns S3 URLs via a dedicated SCC Lambda route GET /g/{event_id}/presign. The guest page JavaScript makes a presigned POST request directly to S3, bypassing Lambda entirely. This reduces Lambda invocations and keeps the upload time-bounded by the presigned URL expiry (typically 1 hour).
Event Creation and Crew Notification
A single SCC event creation call does multiple things:
- Creates the event in DynamoDB
- Assigns crew members