```html

Building a Multi-Tenant Charter Booking Pipeline: Calendar, Events, and Guest Pages Across Distributed S3/CloudFront

What We Built

In a single development session, we implemented an end-to-end booking fulfillment pipeline for a charter reservation system. When a booking arrives via Boatsetter, the system now:

  • Creates an internal calendar entry in JADA (custom scheduling system)
  • Generates a ShipCaptainCrew (SCC) event that auto-notifies crew with magic links
  • Builds a guest-facing confirmation page with time-aware photo upload
  • Deploys that page across two separate S3/CloudFront distributions with different path conventions
  • Sends a summary email to ops with revenue breakdown and crew assignments

The technical challenge wasn't any single component—it was orchestrating authentication across three separate Lambda functions, two S3 buckets, two CloudFront distributions with conflicting path-rewriting rules, and understanding why service keys were being stripped at the CloudFront layer.

Technical Architecture

The Three Systems

JADA (sailjada.com): Internal scheduling, owned by us, hosted on S3 bucket sailjada.com behind CloudFront distribution ID found via Route53. Uses a dashboard Lambda that requires X-Dashboard-Token header for calendar mutations.

ShipCaptainCrew (SCC): Crew management platform with its own Lambda backend, DynamoDB event store, and CloudFront distribution. Crew is notified via magic links embedded in the SCC event creation response. The Lambda validates requests via a SERVICE_KEY_HASH environment variable (bcrypt-style hash of the service key).

Queen of San Diego (queenofsandiego.com): Guest-facing site also on S3/CloudFront. Has a CloudFront Function (not Lambda@Edge) that rewrites paths—any request to /g/{slug}.html gets rewritten to /guests/{slug}/index.html before S3 receives it. This matters.

The CloudFront Header Stripping Problem

When we tried to POST to the SCC event endpoint through the CloudFront distribution, the X-Service-Key header was silently dropped. CloudFront strips custom headers by default unless explicitly whitelisted in the cache policy. The solution: bypass CloudFront entirely and hit the API Gateway endpoint directly. This required:

  • Finding the direct API Gateway URL in SCC Lambda environment variables
  • Using that URL instead of the CloudFront-fronted domain for service-to-service calls
  • Keeping CloudFront in the picture for guest-facing traffic (where we don't need custom headers)

The lesson: when service-to-service APIs live behind CloudFront, either whitelist your headers in the cache policy, or use origin-direct URLs for internal calls.

Infrastructure Details

S3 and CloudFront Configuration

sailjada.com: S3 bucket serves the JADA dashboard. CloudFront distribution ID retrieved from Route53 query. File structure is straightforward—CSS, JS, and HTML at the root.

queenofsandiego.com: S3 bucket stores guest pages. CloudFront Function (not Lambda@Edge) at the distribution level performs path rewriting. The function code checks if the request matches /g/(.+)$ and rewrites to /guests/{capture}/index.html. This is why the guest page must be uploaded as a flat .html file, not in a nested directory structure.

Command to verify the live CloudFront function:

aws cloudfront get-function --name queenofsandiego-path-rewrite

(The actual function name varies; retrieve it from the distribution config.)

Lambda Authentication Pattern

JADA Dashboard Lambda: Validates X-Dashboard-Token header against a hardcoded token in environment variables. Called via direct HTTPS with token in headers.

SCC Lambda: Validates service key using bcrypt hash comparison. The environment variable SERVICE_KEY_HASH stores the hash; the service key itself is in another secret. Event creation endpoint is POST /events and returns crew magic links in the response body.

Example flow (no actual key shown):

curl -X POST https://api-gateway-url/events \
  -H "X-Service-Key: [service-key-here]" \
  -H "Content-Type: application/json" \
  -d '{
    "event_name": "Charter: Guest Name",
    "start_time": "2024-05-30T10:00:00Z",
    "end_time": "2024-05-30T13:00:00Z",
    "notes": "Boatsetter booking, crew assignment details",
    "crew": [{"id": "crew-member-1"}, {"id": "crew-member-2"}]
  }'

The response includes magic-link URLs for each crew member to confirm availability.

Guest Page Generation and Deployment

The guest page is a single-file HTML document stored at /tmp/jada-guest-[booking-id].html during development. It includes:

  • Inline CSS for styling (no external dependencies)
  • A photo upload form that uses presigned S3 URLs
  • Time-aware logic: upload form is only active if the charter is within 7 days
  • Dynamic crew roster pulled from the SCC event at page generation time

The page is deployed to S3 at key /guests/[friendly-url]/index.html (e.g., /guests/carole-may-30-boatsetter/index.html). CloudFront Function rewrites /g/carole-may-30-boatsetter to that path.

Upload example:

aws s3 cp jada-guest-xhqgmdh.html \
  s3://queenofsandiego.com/guests/carole-may-30-boatsetter/index.html \
  --content-type text/html

aws cloudfront create-invalidation \
  --distribution-id [dist-id] \
  --paths "/g/carole-may-30-boatsetter"

The CloudFront invalidation invalidates the rewritten path, ensuring the cache is cleared.

Key Decisions and Trade-Offs

Why bypass CloudFront for service keys? Custom headers through CloudFront require cache policy configuration and cache behavior setup. For internal service-to-service communication with sensitive headers, hitting the origin directly is faster and more secure. External (guest-facing) traffic still goes through CloudFront for DDoS protection and caching.

Why use a CloudFront Function instead of Lambda@Edge? CloudFront Functions are simpler, execute faster (no cold starts), and are sufficient for path rewriting. Lambda@Edge would be overkill here.

Why store the guest page as a flat .html file? The CloudFront Function rewrites to /guests/{slug}/index.html. If we stored the file at the rewritten path, we'd need to upload it as such and manage nested directory structures. Storing it flat and letting CloudFront