Multi-Tenant Charter Management: Synchronizing Calendar, Guest Pages, and Crew Dispatch via Lambda and CloudFront
What Was Done
This session focused on ensuring data consistency across JADA's distributed charter management system for a weekend of back-to-back bookings. The work involved:
- Syncing crew assignments from Google Calendar to DynamoDB event records
- Deploying updated guest pages (HTML) to S3 and invalidating CloudFront caches
- Testing the waiver collection pipeline (guardian/minor support)
- Automating guest and crew notifications via email and SMS
- Verifying end-to-end data flow from calendar → Lambda → DynamoDB → guest/crew pages
The core challenge: JADA uses Google Calendar as the source of truth for crew assignments, but crew and guest pages are served from static S3 + CloudFront, with event metadata stored in DynamoDB. Changes in one system must propagate to all others without manual intervention.
Technical Architecture
Event Data Flow:
- Google Calendar API — stores crew names, times, and captain assignments (read via iCal feed and direct API calls)
- DynamoDB table — event records with denormalized crew, guest contact, waiver status, and timestamps
- S3 buckets — guest pages (HTML templates with embedded CSS/JS) and waiver page assets
- CloudFront distributions — cache layer for guest pages and waiver endpoint
- Lambda functions — orchestrate calendar → DynamoDB sync, trigger email/SMS sends, handle waiver POST requests
- AWS SNS / SES — event notifications and transactional email
Key Technical Changes
1. Calendar Event Patching via Google Calendar API
Crew assignments and event metadata were updated by directly patching Google Calendar event objects. Rather than rely on cached iCal feeds, we used the PATCH /calendar/v3/calendars/{calendarId}/events/{eventId} endpoint:
PATCH https://www.googleapis.com/calendar/v3/calendars/jada-internal%40sailjada.com/events/{eventId}
Content-Type: application/json
Authorization: Bearer {refreshed_oauth_token}
{
"description": "Quinn Male (captain), Darrell (crew)\nArrive by: 2:30 PM\n...",
"summary": "Quinn – Sunset Charter"
}
Why this approach: The iCal feed has a built-in cache; direct API patches bypass cache and update the source immediately. Event IDs had to be corrected from shortened to full format (eventId not the numeric ID) before patching would work.
2. Guest Page Generation and S3 Deployment
Guest pages were generated from templates stored in S3 and deployed back to S3 with updated times and crew info:
/tmp/quinn-guest.html— created with 2:30 PM arrive time, captain + crew names/tmp/danika-guest.html— edited 4× to fix arrive time (3:00 PM), mark as paid, add crew/tmp/jonathan-afternoon.html— created with 4:00 PM arrive time and contact info
Files were then uploaded to the S3 bucket (bucket name: sailjada-guest-pages or equivalent) and served from CloudFront distribution.
3. CloudFront Cache Invalidation
After uploading guest pages, CloudFront cache was invalidated using wildcard paths:
aws cloudfront create-invalidation \
--distribution-id {DISTRIBUTION_ID} \
--paths "/quinn-guest.html" "/danika-guest.html" "/jonathan-afternoon.html"
Why required: CloudFront TTL defaults to 24 hours; without explicit invalidation, users would see stale pages. Invalidation ensures Edge Locations refresh within seconds.
4. DynamoDB Event Record Creation and Updates
Event records in DynamoDB were created or updated with complete metadata:
- Partition Key:
eventId(Google Calendar event ID) - Sort Key:
eventDate(ISO 8601 format) - Attributes:
crew— array of captain + crew objects with names and rolesguestEmail,guestPhone— contact info for waiver/boarding notificationswaiverStatus— pending, collected, signed (tracks waiver completion)pageUrl— S3/CloudFront URL for guest pagearriveTime— departure or arrival deadline
Records were created via a Lambda function that consumed the patched Calendar API response and denormalized crew data into the event record.
5. Waiver Collection and Minor Handling
The waiver endpoint was tested with guardian/minor support. A POST request includes:
POST /waiver
Content-Type: application/json
{
"eventId": "...",
"guestName": "Quinn Male",
"guestAge": 16,
"guardianName": "Parent Name",
"guardianEmail": "parent@example.com",
"waiverSignature": "base64_svg_signature",
"photoUploadCode": "XXXX"
}
The Lambda handler checks age and routes minor waivers to a guardian review flow before storing in DynamoDB. This ensures compliance with liability requirements.
6. Guest and Crew Notifications
After all data was synchronized, automated notifications were sent:
- Email via SES: Boarding instructions with guest page URL and photo upload code
- SMS via SNS: Quick link + photo code (for guests without email on file)
- Message Template: Included arrive-by time, captain name, waiver link, and any special instructions
Why SMS fallback: Email is asynchronous and may bounce; SMS ensures critical pre-charter info reaches guests.
Key Decisions and Rationale
Source of Truth: Google Calendar
Google Calendar was chosen as the source of truth because crew managers already use it; updates in Calendar automatically propagate downstream when Lambda syncs. Storing crew in DynamoDB alone would create dual-entry burden.
Denormalization in DynamoDB
Rather than storing only crew IDs and looking them up on read, full crew objects (names, roles, emails) are denormalized into event records. This trades storage for query speed—crew pages render instantly without N+1 DynamoDB lookups.
Static Pages + CloudFront over Server-Side Rendering
Guest pages are static HTML deployed