```html

Building a Multi-Tenant Charter Booking System: Guest Pages, Event Management, and Cross-Domain CloudFront Distribution

What Was Done

We implemented a complete booking workflow for a charter reservation system that spans three distinct domains and multiple AWS services. Starting from a Boatsetter booking (a third-party charter marketplace), we created:

  • A calendar entry in JADA Internal Calendar with proper authentication
  • An SCC (ShipCaptainCrew) event that auto-notifies crew via magic links
  • A guest-facing booking confirmation page served from a secondary domain
  • A crew-facing checklist and event details page
  • Automated email notifications to all assigned crew members

The challenge: these systems live across multiple S3 buckets, CloudFront distributions, and Lambda functions with different authentication mechanisms. This post details how we unified them.

Technical Architecture

Multi-Domain S3 + CloudFront Strategy

The system uses two primary CloudFront distributions:

  • sailjada.com: Main site (S3 bucket: sailjada.com, CloudFront origin)
  • queenofsandiego.com: Guest-facing pages (separate S3 bucket and CloudFront distribution)

Why split domains? Guest pages need to be branded separately from internal crew tools. By using a dedicated domain, we avoid mixing guest-facing UI with admin interfaces, and we gain independent caching and invalidation control.

Guest Page Path Convention

Guest pages live at /g/{friendly-slug}.html on queenofsandiego.com. The CloudFront function (edge logic) rewrites internal requests transparently. The naming pattern moved from booking-ID-based slugs to friendly names like /g/boatsetter-may-30.html.

The CloudFront function reads the request path, validates it matches the flat .html file convention, and serves the correct object from S3 without exposing the rewrite logic to the client. This keeps URLs human-readable while maintaining strict file organization.

Lambda Authentication: Two Patterns

Two different authentication patterns emerged:

  1. Dashboard Lambda (JADA Calendar): Uses X-Dashboard-Token header. Token must be passed in every request to POST /calendar.
  2. SCC Lambda (Event Management): Uses SERVICE_KEY hashing. The Lambda environment contains SERVICE_KEY_HASH (bcrypt-hashed). Requests must include the plaintext key, which the Lambda function hashes and compares at runtime.

The SCC key is critical: it's generated at Lambda deployment time and stored in environment variables, not in code. The hash_password() function in the Lambda uses bcrypt, so service keys are never stored plaintext anywhere.

Building the Guest Page

The guest page is a static HTML file with embedded JavaScript for photo uploads. Here's the flow:

User visits /g/boatsetter-may-30.html
  ↓
CloudFront function rewrites to /boatsetter-may-30.html (S3 object)
  ↓
Page loads, displays charter details (guest name, time, boat info)
  ↓
Photo upload button → presigned POST URL from /g/ route in SCC Lambda
  ↓
Guest uploads directly to S3 (bypassing Lambda)

The presigned URL is time-aware: it expires after 24 hours. This prevents indefinite upload windows and keeps the booking window bounded. The Lambda function that generates the presigned URL (handle_guest_presign) validates the booking ID and generates a URL scoped to a specific S3 prefix.

The page itself is minimal: guest name, charter duration, boat details, and a photo upload section. No crew details, no financial information—only what the guest needs to know.

SCC Event Creation and Crew Notifications

Creating an SCC event triggers automatic notifications to all assigned crew. This happens via:

POST /events to SCC Lambda (with SERVICE_KEY auth)
  ↓
Lambda creates event in DynamoDB
  ↓
Lambda sends SNS notification to crew topic
  ↓
Each crew member receives email with magic link to their crew page

The magic link includes a session token (temporary, scoped to that booking). Crew click the link and land on a crew-facing page with a checklist, weather briefing, and event details. No password needed—the token in the URL grants access.

One critical detail: we deliberately removed revenue information from the SCC event notes before crew could see them. This required a direct DynamoDB update (rather than using the event update route) because the frontend displays event.notes and we didn't want crew seeing "Revenue: $840.75" or "Captain Fee: $150". The financial details stay in the internal JADA system.

CloudFront Header Stripping Issue

Initial attempts to POST to SCC Lambda through CloudFront failed silently because CloudFront strips custom headers (including Authorization and service key headers) by default.

Solution: Bypass CloudFront and call the API Gateway endpoint directly. The SCC Lambda is fronted by API Gateway (regional endpoint, not CloudFront). By calling the API Gateway URL instead of the CloudFront distribution URL, we avoided the header-stripping problem entirely.

This is a common AWS pattern: CloudFront is great for caching and DDoS protection on GET requests, but for POST/PATCH/DELETE with custom auth headers, direct API Gateway calls are more reliable.

Key Infrastructure Changes

  • S3 bucket: queenofsandiego.com — stores guest-facing HTML pages
  • CloudFront distribution: queenofsandiego.com (separate from sailjada.com) — edge function rewrites /g/ paths
  • Lambda functions:
    • /tmp/scc-lambda-src/lambda_function.py — SCC event management, presigned URLs, crew notifications
    • Dashboard Lambda (hosted separately) — JADA calendar entries
  • DynamoDB table: SCC events table — stores booking metadata, crew assignments, notes
  • SES: Email delivery for crew confirmations and guest confirmations

Financial Calculation and Boatsetter Integration

For this specific booking ($840.75 gross):

  • Crew: 2 × $25/hr × 5 hrs = $250
  • Captain: 1 × $50/hr × 3 hrs = $150
  • Port/tax: 18% of $840.75 = $151.34
  • Net to owner: $289.41

Boatsetter does charge a separate captain fee, and it's possible they collect that from the guest and pass it through. This would reduce your net captain cost. The takeaway: verify in your Boatsetter dashboard whether the captain fee is already paid by Boatsetter or if you're covering it yourself.

What's Next

The system now handles the core workflow automatically