Building a Dynamic Charter Document Pipeline: From Calendar Events to Published S3 Manifests
Over the past development session, I built out a complete document generation and publishing pipeline for JADA Operations' charter management system. This post walks through the architecture, infrastructure decisions, and the specific tooling that makes it work.
What Was Done
The core problem: charter captains and crew need up-to-date manifests and trip sheets before each weekend's departures. Previously, these documents were static or manually generated. The solution involved:
- Fetching charter event data from JADA's internal calendar API
- Generating HTML manifests and trip sheets dynamically from charter details
- Publishing these documents to S3 with proper CloudFront invalidation
- Persisting document metadata and charter records locally for durability
- Integrating with the existing shipcaptaincrew web application's document serving layer
Technical Architecture
Document Generation Pipeline
The pipeline lives in /Users/cb/Documents/repos/jada-ops/ with two primary Python modules:
charter_provisioner.py— orchestrates the entire workflow: fetches calendar events, generates documents, manages S3 publishingsend_charter_emails.py— handles post-publication notifications to crew
The document generation process extracts charter details (passenger names, dates, special provisioning notes, payment status) and renders them into two formats:
- Manifests — passenger rosters with liability info, published as
quinn-male-manifest.html - Trip Sheets — crew operational guides with timing, weather notes, and special requests, published as
quinn-male-trip-sheet.html
Both templates inherit styling from the main shipcaptaincrew application, ensuring consistency across the crew portal.
Calendar Integration
The system queries JADA's internal Google Calendar API to identify weekend charters. The workflow:
1. Authenticate to calendar API with OAuth token refresh logic
2. Query events for a given weekend date range
3. Extract event metadata: charter ID, captain assignment, passenger count
4. Cross-reference with pricing proposals (stored in /jada-ops/proposals/)
5. Generate manifests with all necessary crew and passenger details
This approach keeps the source of truth in the calendar system while allowing documents to be regenerated on-demand without re-entering data.
Infrastructure & Publishing
S3 Document Storage
Documents are published to two S3 locations for redundancy and organizational clarity:
s3://shipcaptaincrew-docs/manifests/quinn-male-manifest.html— primary location, indexed by manifest types3://shipcaptaincrew-docs/crew-page/{event_id}/quinn-male-manifest.html— secondary location, indexed by event for easy crew-page lookup
Both locations use Content-Type: text/html; charset=utf-8 to ensure proper rendering in browsers. The dual-publish strategy provides both direct access and event-scoped queries.
CloudFront Cache Invalidation
Documents are served through a CloudFront distribution (ID and domain names withheld for security). After publishing to S3, the pipeline automatically invalidates the cache:
aws cloudfront create-invalidation \
--distribution-id {DISTRIBUTION_ID} \
--paths '/manifests/quinn-male-manifest.html' '/crew-page/{event_id}/*'
This ensures that crew viewing the documents see fresh content within 60 seconds, critical for last-minute passenger changes or special notes.
Lambda Integration
The existing Lambda function at /Users/cb/Documents/repos/sites/queenofsandiego.com/tools/shipcaptaincrew/lambda_function.py serves as the API backend. Key modifications:
- Extended the document retrieval logic to check both S3 prefixes
- Modified
build_event_pagesto dynamically link to published manifests - Updated the
handle_get_docroute to resolve event IDs correctly
The Lambda function now acts as both a document gateway and a client-side router—the frontend SPA requests documents via a clean REST endpoint, and Lambda resolves the correct S3 key without exposing bucket structure to clients.
Local Persistence & Durability
To avoid losing charter state, we store metadata locally:
/Users/cb/Documents/repos/jada-ops/quinn-male/quinn-male-manifest.html— local archive copy/Users/cb/Documents/repos/jada-ops/quinn-male/quinn-male-trip-sheet.html— local crew notes/Users/cb/Documents/repos/agent_handoffs/projects/quinn-male-charter.md— charter metadata and decision log/Users/cb/Documents/repos/jada-ops/weekend-charters-readiness-2026-05-29.md— weekend readiness report aggregating all active charters
This dual-store approach (local + cloud) allows the team to reconstruct any charter's documents if S3 becomes temporarily unavailable, and provides an audit trail of all charter iterations.
Key Technical Decisions
Why HTML Over PDF
We chose HTML manifests over PDF generation because:
- HTML renders natively in browsers without external dependencies
- Mobile crew viewing the documents doesn't require a PDF reader
- CSS can be styled consistently with the rest of the shipcaptaincrew application
- Future interactive elements (notes, checkboxes) are trivial to add
Why Dual S3 Locations
Publishing to both /manifests/ and /crew-page/{event_id}/ enables two access patterns:
- Direct manifest links in crew communications
- Event-scoped document discovery for the crew portal UI
The cost of duplication is negligible; the benefit is flexibility.
Why CloudFront Invalidation
CloudFront's default TTL could leave stale documents cached for hours. Given that charter details can change rapidly (passenger cancellations, weather updates), we invalidate on every publish. This trades cache efficiency for operational accuracy.
Why Local Archive
Storing copies in the git repository allows:
- Version control of all document changes
- Offline charter reference for developers and operations
- Decoupling local tooling from S3 availability during development
Command Examples
Publishing a manifest and invalidating CloudFront:
# Generate and stage the manifest locally
python charter_provisioner.py --charter quinn-male --action generate
# Publish to both S3 locations
aws s3 cp quinn-male-manifest.html \
s3://shipcaptaincrew