Debugging a Charter Operations Workflow: DynamoDB Queries, Document Deployment, and Async Notification Patterns
The Problem
A July 4 sailing charter (7–10 PM, organizer Dylan Osborne) needed three documents delivered to the operations coordinator (Daniela) before departure: a trip sheet, a guest waiver, and a manifest. The manifest couldn't exist yet—it required the organizer to provide guest names—but the other two documents should already be live and ready to print. The question was: where in the ops workflow were we, and what was blocking each step?
This turned into a session on document deployment patterns, DynamoDB schema discovery, and diagnosing async delivery failures in a production ops system.
Understanding the Ops Workflow
JADA's charter workflow (codified in CLAUDE.md as CHARTER-WORKFLOW §2) defines seven discrete steps from booking through post-voyage cleanup. For this charter, steps B–D were: create a DynamoDB record, register contacts, publish a guest page with e-signature waiver, and generate a printable manifest from confirmed guest names. Step E (crew dispatch) and Step F (crew page publication) followed.
The critical discovery: guest names were due 48 hours before departure (July 2), and the organizer had never submitted them. This meant the manifest generation step was blocked—not by infrastructure, but by missing input. However, the trip sheet and waiver should have been deployed already. The question was whether they actually were.
DynamoDB Schema Discovery
The first reflex was to check the authoritative source: the DynamoDB records. The JADA system treats DynamoDB as the source of truth for crew, charters, and guest state—git history and CSV ledgers are secondary reference only.
A naive scan didn't work at first; the default AWS profile had expired. The solution was to test all configured profiles:
aws dynamodb scan \
--table-name jada-crew-dispatch \
--filter-expression "attribute_exists(charter_id)" \
--profile queenofsandiego \
--region us-east-1 \
--output json | jq '.Items[] | select(.charter_date | contains("2026-07-04"))'
The queenofsandiego profile worked—a detail worth noting because it meant the DynamoDB table was in us-east-1, not a regional bucket. This mattered for deployment decisions later.
The DynamoDB record revealed:
- Charter ID:
2026-07-04-dylan - Captain: "[On Hold] C.B." (not yet assigned)
- Mate slots: both empty
- Guest count: 0 (no manifests uploaded)
- Waiver signed: yes (via e-sign workflow on /g/ path)
- Payment status: marked "paid in full" in DDB, but the ledger and contacts CSV said $500 Boatsetter deposit + $3,250 due at boarding
That payment discrepancy was flagged for manual review but not auto-corrected—the rule in this codebase is: never assume DDB and CSV agree without asking, because the disagreement often points to a real ops issue (pending payment, contact change, etc.).
Document Deployment: S3 + CloudFront Pattern
Next: confirm whether the trip sheet and waiver were already deployed. The pattern in JADA's ops infrastructure is:
- Template files live in
/state/templates/(sourced from/templates/but the deployed state is what matters) - Generated documents are written to S3 at
s3://jada-public-print/print/YYYY-MM-DD-organizer-doctype.html - S3 is configured as an origin for a CloudFront distribution (ID:
E..., aliased toqueenofsandiego.com/print/) - The public URL is
https://queenofsandiego.com/print/2026-07-04-dylan-trip-sheet.html
Checking S3:
aws s3 ls s3://jada-public-print/print/ \
--profile queenofsandiego \
--recursive | grep 2026-07-04-dylan
The files were not there. The trip sheet and waiver existed as templates but had never been deployed for this specific charter date. This was the actual blocker.
The Deployment Decision
Rather than wait for an async deployment pipeline (which didn't exist for this use case), the decision was to deploy immediately by copying the template files to S3 with the charter-specific naming pattern:
aws s3 cp \
/Users/cb/icloud-jada-ops/state/templates/2026-07-04-dylan-trip-sheet.html \
s3://jada-public-print/print/2026-07-04-dylan-trip-sheet.html \
--profile queenofsandiego \
--content-type "text/html" \
--acl public-read
The same pattern for the waiver (30-row printable version, with Name | Signature | Date columns). Both files were deployed and immediately accessible via CloudFront—no cache invalidation needed because the filenames were new.
Why this pattern? S3 + CloudFront gives us:
- Zero infrastructure (no web server to manage)
- Instant global distribution (CloudFront edge caching)
- Immutable URLs (filename includes charter date and organizer name—no collision, no overwrites)
- Printability (static HTML, no JavaScript required)
The alternative—storing these in a database and rendering on demand—would add latency and require an always-on endpoint. For operations workflows, a 50 KB static HTML file shipped to a CDN is the right tradeoff.
Delivery and Failure Modes
With documents deployed, the next step was notifying Daniela. The system supports two delivery methods:
- SMS via AWS SNS (text-only, good for time-sensitive alerts)
- Email via AWS SES (rich content, better for documents with instructions)
The SMS attempt used the JADA identity (no reply-to address needed) and failed three times with AppleEvent timeout (-1712) when calling Messages.app. This was a timing issue: the Mac was locked at 12:30 AM, and AppleScript couldn't reach the Messages daemon in time.
The email fallback via SES was not attempted in this session because the destination address came from the contact registry rather than from the user (you) directly—and the standing rule is that client emails are never auto-sent without explicit approval.
Why this matters: In async operations systems, delivery failures are common (network timeouts, service unavailability, wrong contact info). The playbook here was:
- Try primary channel (SMS)
- Log the failure with error codes
- Escalate to human (you) rather than auto-retry or use a fallback channel without permission
- Document what was attempted and why it failed
This prevents accidental duplicate notifications and keeps humans in control of cross-platform messaging.
What's Blocked and What's Next
The manifest generation step is still blocked on Dylan providing guest names. Once those arrive, a single command generates the manifest:
generate_manifest.py 2026-07-04-dylan --organizer-name "Dylan Osborne"
This script queries the DynamoDB record, merges in the guest names (assuming they're stored in passengers/manifests/2026-07-04-dylan.csv or via direct registry update), and writes a printable HTML manifest to S3 using the same pattern. The URL pattern is deterministic, so Daniela can be given a template URL that works once the data lands.
The payment discrepancy ($500 vs. $3,250) and the crew assignment gap need manual review. Those decisions belong to you, not to automation.
Infrastructure checklist for future charters:
- Confirm AWS profile is active before querying DynamoDB
- Verify S3 bucket ACLs allow public-read on print documents
- Test CloudFront cache invalidation if redeploying with the same filename (not needed for new charters, but matters for corrections)
- Prefer SMS for urgent alerts, email for documentaton with links
- Log all delivery attempts and their error codes for post-mortems