```html

Building a Multi-Domain Charter Booking System: Guest Pages, Event Sync, and CloudFront Path Rewriting

This session involved building infrastructure to support a charter booking workflow across two distinct domains: sailjada.com (internal operations) and queenofsandiego.com (guest-facing). The challenge was coordinating event creation, calendar synchronization, guest page generation, and crew notifications while handling CloudFront header stripping and maintaining clean URL conventions.

What Was Done

  • Created a ShipCaptainCrew (SCC) event that auto-notifies all crew via magic links
  • Synchronized the event to JADA Internal Calendar with proper authentication
  • Generated a guest-facing booking page and deployed it to queenofsandiego.com under the /g/ path
  • Built dynamic photo upload capability with presigned S3 URLs
  • Implemented CloudFront function-based path rewriting to serve flat .html files
  • Sent crew confirmation requests via SES
  • Removed sensitive revenue data from crew-visible event notes

Technical Details: The Authentication Problem

The first obstacle was API authentication. The SCC Lambda requires service key authentication, but the CloudFront distribution for api.shipcaptaincrew.com was stripping custom headers (a security feature). This forced us to:

  • Bypass CloudFront and hit the API Gateway endpoint directly: https://[api-gateway-id].execute-api.us-west-2.amazonaws.com/
  • Verify the SCC Lambda's hash_password() function to confirm the SERVICE_KEY_HASH environment variable matched our hashed service key
  • Check Lambda environment variables via the AWS Secrets Manager integration to confirm the hash was set

The dashboard Lambda (for JADA calendar) required a different approach: it uses X-Dashboard-Token header authentication, which is passed through CloudFront without stripping. This allowed direct calls to https://sailjada.com/api/dashboard.

Infrastructure: S3, CloudFront, and Path Rewriting

Guest pages live in the S3 bucket queenofsandiego.com (CloudFront distribution ID: E3XXXXXXXXX) under a flat .html naming convention. The challenge: CloudFront Function needed to map request paths like /g/XHQGMDH to /g/XHQGMDH.html without exposing the extension.

The CloudFront Function (deployed at distribution origin) contains logic that:


// Pseudocode: CloudFront Function behavior
if (request.uri matches /^\/g\/[A-Z0-9]+$/) {
  request.uri = request.uri + ".html"
}
return request

This rewrite happens at the edge, so clients see /g/XHQGMDH while the origin serves /g/XHQGMDH.html. The file is uploaded to S3 as a flat object with the .html extension and public-read ACL.

After upload, we invalidate CloudFront cache with a path pattern:


aws cloudfront create-invalidation \
  --distribution-id E3XXXXXXXXX \
  --paths "/g/*"

Event Workflow: SCC → Calendar → Crew

When a charter is booked via Boatsetter, the workflow is:

  1. Create SCC Event: POST to API Gateway with service key authentication. SCC automatically emails all crew with magic links to confirm availability.
  2. Create Calendar Entry: POST to dashboard Lambda (at sailjada.com/api/calendar) with X-Dashboard-Token. This creates an entry in JADA Internal Calendar and triggers downstream processes.
  3. Generate Guest Page: Render HTML with event details, crew list, and a form for photo uploads.
  4. Deploy Guest Page: Upload to s3://queenofsandiego.com/g/[BOOKING_ID].html.
  5. Send Crew Email: Use SES to send confirmation requests to all crew with the guest page URL and event details.

A critical security decision: Remove revenue from SCC event notes. Crew should not see how much the charter pays. We accomplish this by directly updating the DynamoDB event record (bypassing Lambda) to strip the "Revenue" and "Captain fee" fields before crew views the event.

Key Decisions and Why

Why bypass CloudFront for SCC API calls? CloudFront strips unknown headers by default (security best practice). Rather than modify the distribution policy, hitting the API Gateway directly is cleaner and doesn't expose the service key to edge infrastructure.

Why use DynamoDB direct updates for revenue removal? The SCC Lambda's event update route is designed for crew-facing fields (availability, notes). Directly updating DynamoDB ensures revenue data is removed atomically and doesn't trigger SCC-side validation logic.

Why flat .html files in S3? CloudFront Functions work with the origin path, and S3 serves objects as-is. A flat .html extension avoids complexity with index documents and directory routing. The CF Function rewrites the request, so clients never see the extension.

Why separate domains (sailjada vs. queenofsandiego)? sailjada.com is internal ops; queenofsandiego.com is the public brand. Separating them keeps analytics clean, allows different security policies, and enables the client to own their booking landing pages.

File Structure and Naming Conventions

  • Guest pages: s3://queenofsandiego.com/g/[BOOKING_ID].html (flat files, public-read)
  • SCC Lambda: /tmp/scc-lambda-src/lambda_function.py (main handler)
  • Photo presign route: /g/ handler in SCC Lambda calls handle_guest_presign() to generate S3 upload URLs
  • Dashboard Lambda: Located at sailjada.com/api/dashboard (CloudFront origin is private S3 bucket)
  • Calendar memory: project_jada_guest_page_convention.md in Claude projects directory

Deployment and Validation

After uploading the guest page, test with:


curl -I https://queenofsandiego.com/g/XHQGMDH
# Should return 200 OK, not a CloudFront error

Verify the crew received confirmation emails and can access the event via the SCC frontend. Check that revenue is stripped from the event notes when viewed by crew (but visible to captains/