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-Tokenheader 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_HASHin 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