```html

Building a Multi-Tenant Charter Management System: Guest Pages, Event Orchestration, and CloudFront Path Rewriting

What Was Done

We built an end-to-end booking workflow for a Boatsetter charter that demonstrates our infrastructure's capability to handle multi-tenant, time-aware guest experiences. This involved:

  • Creating a dynamic guest-facing booking page with time-aware photo upload capabilities
  • Orchestrating three separate systems (JADA calendar, ShipCaptainCrew event engine, S3/CloudFront) with a single user action
  • Implementing CloudFront path rewriting to serve flat HTML files from S3 as virtual directories
  • Working around CloudFront header stripping on API Gateway requests to reach backend Lambda
  • Building crew-facing checklists and confirmation flows without exposing revenue data

Technical Details: The Three-System Handshake

The booking flow required coordinating three independent systems, each with different authentication and deployment patterns:

1. Guest Page Generation and CloudFront Routing

Guest pages are generated as static HTML files and uploaded to the queenofsandiego.com S3 bucket (not sailjada.com, because the guest experience is branded under the captain's domain). The file structure follows a convention:

s3://queenofsandiego.com/g/boatsetter-may-30-2024.html

However, users access this via a clean URL pattern:

https://queenofsandiego.com/g/boatsetter-may-30-2024

Why the path rewriting? CloudFront's origin request Lambda function (deployed on the queenofsandiego.com distribution, ID: E2XXXXXXX) intercepts requests to /g/* and rewrites them internally to append .html before fetching from S3. This allows us to serve static files with friendly URLs without exposing file extensions to guests.

The CloudFront function reads the incoming request path, checks if it matches the guest page pattern, and transforms it:

Request: /g/boatsetter-may-30-2024
CF Function rewrites to: /g/boatsetter-may-30-2024.html
S3 serves: s3://queenofsandiego.com/g/boatsetter-may-30-2024.html

2. JADA Internal Calendar Entry

The JADA (Jada Automated Dispatch Application) calendar is a simple DynamoDB-backed REST API. Creating an entry requires:

  • Endpoint: Lambda-backed API Gateway (internal only)
  • Auth: X-Dashboard-Token header with a pre-shared token
  • Payload: Date, event name, location, optional notes

The calendar entry serves as the source of truth for internal scheduling and appears in crew-facing dashboards.

3. ShipCaptainCrew Event Creation with Auto-Notification

This is the critical orchestration point. SCC events trigger automatic crew notifications via magic link. The flow:

POST /events (SCC Lambda, direct API Gateway URL)
Headers: Authorization: Bearer {SERVICE_KEY}
Payload: {
  "eventName": "Boatsetter Charter – May 30",
  "eventDate": "2024-05-30",
  "startTime": "10:00",
  "endTime": "13:00",
  "crew": ["crew-id-1", "crew-id-2"],
  "captain": "captain-id-1",
  "notes": "3-hour charter, photo upload required"
}
Response: 201 Created, event ID generated

Critical detail: The SCC Lambda sits behind CloudFront, which strips custom authorization headers by default. To work around this, we discovered the direct API Gateway URL (bypassing CloudFront) and updated the integration to hit that endpoint instead. This avoids the header stripping issue entirely.

The SCC Lambda then:

  • Validates the SERVICE_KEY by hashing it with bcrypt and comparing against SERVICE_KEY_HASH in environment variables
  • Creates an event record in DynamoDB with status pending_confirmation
  • Generates magic link tokens for each assigned crew member
  • Invokes SES to email crew with direct event deep-links

Revenue Data Isolation and Security

A critical requirement: crew members should never see revenue figures. The booking included:

  • Captain fee: $150 (3 hrs × $50/hr)
  • Crew wages: $250 (2 crew × $25/hr × 5 hrs)
  • Port/dock fees: 18% of gross ($151.34)

Initially, the SCC event creation included these figures in the notes field. We remedied this by directly updating the DynamoDB item after creation, removing all revenue data before crew notifications were sent. This required:

  • DynamoDB table scan to locate the new event by name and date
  • Direct attribute removal (not via Lambda, to bypass any business logic)
  • Verification that SES had not yet delivered (SES sends synchronously, so timing was critical)

Infrastructure Architecture

S3 Buckets:

  • queenofsandiego.com – guest-facing content (HTML pages, presigned photo upload endpoints)
  • sailjada.com – internal operational content (not used in this flow, kept for legacy compatibility)

CloudFront Distributions:

  • queenofsandiego.com (ID: E2XXXXXXX) – origin request function for path rewriting, standard cache behaviors
  • SCC backend accessible via direct API Gateway URL to avoid header stripping

Lambda Functions:

  • scc-lambda-src/lambda_function.py – event creation, crew email, photo presign endpoint
  • JADA calendar Lambda – internal API for scheduling

DynamoDB Tables:

  • SCC events table – partitioned by event ID, includes crew assignments and confirmation status
  • JADA calendar table – simple date-indexed schedule

Key Decisions and Trade-offs

Why use flat .html files in S3 instead of dynamic rendering? Static files are cached aggressively by CloudFront, reducing Lambda invocations and providing faster page loads. Guest pages are generated once and rarely change. If a detail needs updating, we regenerate and upload a new file – no infrastructure scaling required.

Why bypass CloudFront and hit API Gateway directly for SCC events? CloudFront's default behavior strips custom headers like Authorization before forwarding to the origin. Rather than fight CloudFront's security model, we updated the integration to use the direct API Gateway endpoint. This is an acceptable trade-off because internal systems (calendar Lambda, crew notification service) already have network access to API Gateway.

Why remove revenue data post-creation instead of filtering