Building a Charter Booking Flow: Automating Calendar, Crew Notifications, and Guest Pages Across Multiple Domains
When a new charter books through Boatsetter, we need to spin up infrastructure artifacts across three separate systems: an internal calendar entry for planning, a crew-facing event with auto-notifications, and a guest-facing landing page with photo upload capability. This post walks through the technical decisions and implementation details that make this multi-system coordination work seamlessly.
The Problem: Manual Coordination Across Silos
Before automation, booking a charter meant:
- Manually creating a calendar entry in JADA Internal
- Creating a separate event in ShipCaptainCrew to notify crew
- Building and deploying a custom guest page for each booking
- Manually emailing crew confirmation requests
This is error-prone and doesn't scale. We needed a single source of truth that could drive all downstream artifacts automatically.
Architecture: Three Independent Systems, One Data Flow
We built a workflow that creates three artifacts in parallel when a booking arrives:
- JADA Internal Calendar: Planning view for the captain and operations
- ShipCaptainCrew Event: Crew notification hub with magic links and role-based access
- Guest Landing Page: Hosted on queenofsandiego.com, includes photo upload integration
Each system has its own authentication, storage, and API surface. The orchestration happens in a local script that calls each API in sequence.
JADA Internal Calendar: Adding Events via Lambda
The JADA system exposes a Lambda function (part of the dashboard infrastructure) that accepts POST requests to create calendar entries. The endpoint requires the X-Dashboard-Token header.
Request structure:
POST https://dashboard-api.example.com/calendar
X-Dashboard-Token: [service-key-hash]
Content-Type: application/json
{
"title": "3hr Charter - Boatsetter",
"date": "2024-05-30",
"start_time": "10:00",
"end_time": "13:00",
"notes": "Guest: [Name], Boatsetter ID: [ID]"
}
The dashboard Lambda sits behind CloudFront distribution d123abc.cloudfront.net. Critically, the CF distribution strips custom headers, so direct calls to the origin S3 bucket's API Gateway URL are necessary for authentication to work.
Why this matters: CloudFront by default blocks all headers except a whitelist (Accept, Host, User-Agent, etc.). We discovered this when the service key header wasn't reaching the Lambda. The solution was to bypass CloudFront and hit the API Gateway URL directly: https://api.dashboard.example.com/calendar.
ShipCaptainCrew: Event Creation with Crew Auto-Notification
ShipCaptainCrew (SCC) is a crew management and task coordination system. Creating an event there automatically sends magic-link invites to all crew members assigned to that event.
Request structure:
POST https://api.shipcaptaincrew.com/events
Authorization: Bearer [service-key-hash]
Content-Type: application/json
{
"event_name": "3hr Charter - Boatsetter",
"date": "2024-05-30",
"start_time": "10:00",
"end_time": "13:00",
"crew": ["crew_id_1", "crew_id_2"],
"notes": "No host/hostess, bare minimum crew requested"
}
The SCC Lambda validates the service key by hashing it and comparing against the SERVICE_KEY_HASH environment variable. This hash is computed using the hash_password function in the Lambda handler (located in /tmp/scc-lambda-src/lambda_function.py).
Authentication flow:
- Client sends raw service key in Authorization header
- Lambda receives request and extracts the key from the header
- Lambda calls
hash_password(key)and compares result to environment variable - If hashes match, request is processed; otherwise, 401 Unauthorized
The event creation triggers SCC's internal notification engine, which looks up crew members by ID and sends email invites with magic links. These links bypass password auth and land crew directly in the event detail page.
Guest Landing Page: CloudFront Path Rewriting and S3 Hosting
The guest page lives on queenofsandiego.com/g/friendly-booking-slug. This requires careful CloudFront function setup to rewrite requests to the correct S3 object.
Architecture:
- S3 bucket:
queenofsandiego.com - CloudFront distribution ID:
E2ABCD1234XYZ - CloudFront function:
path-rewriter(viewer request) - S3 object path:
/g/[booking-slug].html
The CloudFront function intercepts requests to /g/* and rewrites them to the S3 object path:
function handler(request) {
const uri = request.uri;
if (uri.startsWith('/g/')) {
const slug = uri.split('/')[2];
request.uri = `/g/${slug}.html`;
}
return request;
}
This convention allows us to upload flat HTML files (e.g., /g/xhqgmdh.html) while serving them at clean URLs (e.g., /g/xhqgmdh). The slug is generated from the booking ID or assigned manually for friendlier URLs.
Why S3 + CloudFront instead of a dynamic web server? Static hosting is cheaper, faster (CDN edge caching), and eliminates server management. For pages that need dynamic content (like real-time crew confirmations), we embed API calls in JavaScript that run client-side.
Photo Upload Integration
The guest page includes a photo upload widget. Instead of posting directly to S3 (which would require embedding credentials), we use a presigned URL approach:
- Guest clicks "Upload Photo"
- JavaScript calls
GET /g/[booking-id]/presignon the SCC backend - SCC Lambda returns a temporary presigned URL for that specific S3 object
- Browser uploads directly to S3 using the presigned URL
- S3 expires the URL after 15 minutes
This keeps credentials server-side and limits exposure. The presign route in the SCC Lambda (handle