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 roles
    • guestEmail, guestPhone — contact info for waiver/boarding notifications
    • waiverStatus — pending, collected, signed (tracks waiver completion)
    • pageUrl — S3/CloudFront URL for guest page
    • arriveTime — 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