Multi-Service Calendar Sync & Guest Page Deployment: Reconciling Google Calendar, DynamoDB, and CloudFront for Weekend Charter Operations

What Was Done

This session involved a comprehensive sync across three independent systems managing weekend sailing charter data: Google Calendar (source of truth for crew/guest schedules), DynamoDB (operational state for waivers and crew dispatch), and CloudFront-distributed guest/crew pages (S3-hosted HTML). The work ensured that calendar changes propagated correctly to all downstream systems, guest waivers functioned for both adults and minors, and boarding pages accurately reflected crew assignments and timing.

The Problem: Distributed State Across Three Systems

Sailjada manages charters through a decentralized architecture:

  • Google Calendar — crew scheduling, guest names, event descriptions (source of truth)
  • DynamoDB — event metadata, crew dispatch state, waiver submission tracking (operational layer)
  • S3 + CloudFront — public guest boarding pages and crew assignments (customer-facing)

When crew assignments changed mid-week (e.g., replacing Darrell with Gene on June 14's Ariel Loback event), updates had to cascade: Calendar → Lambda → DynamoDB → CloudFront invalidation → guest page refresh. A single missed step meant guests received outdated crew information or waivers failed to process correctly.

Technical Details: The Sync Pipeline

Step 1: Calendar Event Retrieval & Parsing

The session began by fetching the JADA Internal calendar via two mechanisms:

  • iCal endpoint — parsed for event metadata (titles, times, descriptions)
  • Google Calendar API (REST) — used for programmatic updates with OAuth credentials

The iCal approach was initially preferred because it doesn't require credential refresh, but for patching crew names or event details, the REST API was necessary. This dual-path design reflects a common pattern: read from a stable, cacheable format (iCal); write through an authenticated API.

Event IDs from iCal needed to be converted to the Google Calendar API format. Example command:

curl -H "Authorization: Bearer $GOOGLE_OAUTH_TOKEN" \
  https://www.googleapis.com/calendar/v3/calendars/jada%40sailjada.com/events \
  -X PATCH -d '{"summary":"Ariel Loback (June 14)","description":"Crew: Gene, ..."}' \
  --header "Content-Type: application/json"

The OAuth token refresh was critical: the session checked env for cached credentials and refreshed them when stale, using the refresh token stored in the environment.

Step 2: DynamoDB State Management

For each charter event, a record exists in DynamoDB (table name abstracted, but accessed via Lambda) containing:

  • Event ID (Google Calendar ID)
  • Event name and date/time
  • Crew roster (captain, crew members)
  • Guest count and waiver status
  • Boarding page URL

When crew changed on the calendar, the DynamoDB record had to be updated before the guest page would reflect it. The session used AWS Lambda functions to perform these updates, avoiding direct DynamoDB CLI calls in production. Example workflow:

aws lambda invoke \
  --function-name jada-crew-dispatch-update \
  --payload '{"eventId":"XHQGMDH", "crew":["Gene","Santiago"]}' \
  /tmp/lambda-response.json

The Lambda function (source in internal repo) performed the DynamoDB write, then returned a status indicating success or conflicts. If the function timed out or failed, the session fell back to checking CloudWatch logs.

Step 3: Guest Page Generation & Deployment

Guest pages are static HTML files generated from templates and deployed to S3. Three pages were modified this session:

  • /tmp/quinn-guest.html — new guest page for Quinn's Saturday event
  • /tmp/jonathan-afternoon.html — Jonathan's Sunday afternoon charter
  • /tmp/danika-guest.html — Danika's Saturday event (edited 4 times as crew/times were corrected)

Each file contains embedded crew names, arrival times, waiver form URLs, and photo upload codes. The template pattern:

<h1>{{ guest_name }} — {{ charter_date }}</h1>
<p>Crew: <strong>{{ captain_name }}</strong>, {{ crew_members }}</p>
<p>Arrive by: {{ arrive_time }}</p>
<a href="/waiver?eventId={{ event_id }}&waiverId={{ waiver_id }}">
  Start Waiver
</a>

Files were then uploaded to S3:

aws s3 cp /tmp/quinn-guest.html \
  s3://sailjada-charters/guests/quinn-saturday-51.html \
  --cache-control "max-age=3600"

Note the cache-control header: 1-hour max-age ensures CloudFront respects updates without stale serves.

Step 4: CloudFront Invalidation

After each S3 update, CloudFront's cache had to be invalidated. The session invalidated all three guest pages:

aws cloudfront create-invalidation \
  --distribution-id E1234ABCDEFG \
  --paths "/guests/quinn-saturday-51.html" \
  "/guests/jonathan-afternoon.html" \
  "/guests/danika-guest.html"

Distribution ID E1234ABCDEFG (redacted) is the CloudFront distribution serving *.sailjada.com. Invalidation typically propagates in 30–60 seconds; the session verified this by checking HTTP status:

curl -I https://sailjada.com/guests/quinn-saturday-51.html

Step 5: Waiver Processing & Minor Handling

A critical discovery: the waiver form endpoint needed to support minor/guardian data. The Lambda function handling waiver submissions (located at `/handlers/waiver.js`) was updated to:

  • Check if guest is under 18 (from DynamoDB event record)
  • Require guardian name, contact, and signature if minor
  • Store guardian data in a separate DynamoDB attribute

The session tested this live by submitting a waiver for Quinn (a minor) with guardian data:

curl -X POST https://sailjada.com/api/waiver \
  -d '{"eventId":"XHQGMDH","guestName":"Quinn","age":16,"guardianName":"Sarah","guardianPhone":"..."}' \
  --header "Content-Type: application/json"

The response confirmed the waiver was created and a photo-upload code was issued.

Step 6: Crew & Guest Communication

Once pages were live and waivers functional, the session sent boarding emails: