Automating Charter Proposal Generation and Crew Dispatch with Headless Chrome and DynamoDB

This post documents the technical workflow for generating branded PDF proposals and orchestrating crew dispatch through our ShipCaptainCrew API — a multi-stage process that touches file storage, PDF rendering, S3 deployment, and database operations.

What Was Done

We needed to accomplish two parallel objectives for an incoming July 4th charter:

  • Generate a professional PDF proposal from an HTML template and deploy it to our CDN for client review
  • Create a charter event in ShipCaptainCrew, then initiate crew assignment and notification workflows

The work involved understanding our existing proposal format, setting up the render pipeline, querying crew rosters from DynamoDB, and orchestrating API calls to create events and generate authenticated invite links.

Technical Details: Proposal Generation Pipeline

Our proposal system uses a template-based approach stored in iCloud Drive at /Users/cb/Library/Mobile Documents/com~apple~CloudDocs/jada-ops/proposals/. We examined existing Dylan proposals to understand the HTML structure and branding conventions used by queenseadiegos.com clients.

The rendering pipeline works as follows:

HTML Template → Headless Chrome (--headless --print-to-pdf) → PDF Binary → S3 /g/ path → CloudFront Distribution

We identified that existing proposals use a direct booking format (DIRECT HTML variant) that includes pricing breakdown, deposit status, and crew availability. The template structure follows semantic HTML with CSS for print media, making it ideal for headless Chrome rendering.

Key command executed:

google-chrome --headless --disable-gpu --print-to-pdf=/output/path/dylan-july4-confirmed.pdf /local/path/dylan-july4-confirmed.html

This approach was chosen over server-side PDF libraries (like wkhtmltopdf) because:

  • Chrome headless rendering matches our web preview exactly — no CSS compatibility issues
  • Faster iteration: we can test in browser first, then render
  • Handles complex layouts including images and embedded fonts
  • Lower infrastructure overhead compared to maintaining a dedicated rendering service

The rendered PDF is saved locally to jada-ops/proposals/dylan-july4-confirmed.pdf for local archival, then deployed to S3.

Infrastructure: S3 and CDN Deployment

Proposals are deployed to our S3 bucket under the /g/ path prefix, which routes through CloudFront for fast client delivery. The deployment process:

  • Upload PDF to S3 bucket with content-type application/pdf
  • Set cache control headers to allow browser caching while preserving freshness
  • CloudFront distribution automatically serves from edge locations
  • Generate CDN URL for client email distribution

This avoids serving PDFs from our main application server and uses our existing CDN infrastructure optimized for static assets. The /g/ path convention (presumably "guest" or "generate") keeps proposals separate from other S3 content.

Technical Details: Crew Dispatch Workflow

Parallel to PDF generation, we queried our crew data to initiate assignment for July 4th. Our crew roster lives in DynamoDB with the following data flow:

DDB Crew Table → API /api/crew/roster endpoint → Filter by role (captains) → Generate magic links → Send notifications

First, we inspected the DDB schema by listing primary keys to understand the data structure:

jada-ops/
├── state/
│   └── charter-data/ (contains event records)
└── (other operational files)

We fetched the crew roster from our ShipCaptainCrew API at the /api/crew/roster endpoint, which returned structured crew records. The roster includes captain names, emails, and availability status.

Critical filtering step: we extracted only captain-tier crew members from the full roster, since deck crew and support staff have different assignment workflows. The extraction logic:

  • Query roster endpoint for all crew
  • Filter where role == 'captain' or equivalent permission level
  • Collect email addresses for notification dispatch
  • Verify July 4th date against crew availability calendars in DDB

Charter Event Creation and Magic Link Generation

We created a charter event in ShipCaptainCrew via POST /api/events with payload including:

  • Event date: July 4, 2026
  • Charter type: Dylan (client identifier)
  • Vessel: extracted from proposal metadata
  • Crew requirements: captain + support crew

The API responded with an event ID. We then used the /api/auth/magic endpoint to generate authenticated invite links for each captain email address. Magic links are time-limited tokens that allow crew to accept/decline charter assignments without traditional login:

POST /api/auth/magic
{
  "email": "captain@example.com",
  "event_id": "event-uuid",
  "action": "charter_invite"
}
Response: { "magic_link": "https://shipcc.sailjada.com/accept/TOKEN" }

Magic links were chosen over traditional email + password flows because:

  • Crew often work with multiple operators; reducing password burden improves adoption
  • Links can expire automatically (typically 24-48 hours)
  • Simplifies mobile access for crews at sea with limited app updates
  • Each link is single-use, reducing credential compromise surface

We then probed the notification API to identify the correct endpoint for dispatching invitations:

POST /api/notifications/send
or
POST /api/events/{event_id}/notify_crew

Testing via Python identified which endpoint successfully queued notifications for async delivery.

Key Decisions

Why local file archival plus S3 deployment: Local copies in jada-ops/proposals/ serve as operational history and backup. This becomes critical if S3 access is unavailable or for audit trails on sensitive proposals.

Why DynamoDB for crew data: Single source of truth for crew scheduling, availability, and role assignments. DDB's on-demand pricing suits our variable crew query patterns.

Why separate magic link generation from event creation: Decoupling these steps allows us to create the event first, then asynchronously generate and send links. If a captain email bounces, we don't block event creation.

Headless Chrome over server-side rendering: We could have built a dedicated PDF microservice, but headless Chrome leverages our existing web stack. Engineers already understand CSS/HTML; debugging is straightforward.

What's Next

Future improvements to this workflow:

  • Implement event creation idempotency to prevent duplicate events if API calls retry
  • Add crew preference matching (some captains prefer specific vessel types or regions)
  • Build a dashboard in jada-ops to track proposal status: generated, sent, viewed, accepted
  • Automate follow-up notifications if crew hasn't responded within 12 hours