Charter Booking Platform Hardening: Database Normalization, LaunchAgent Security, and Deployment Pipeline Fixes
What Was Done
This session focused on stabilizing the Queen of San Diego charter booking platform ahead of a time-critical sailing (Dylan Osborne, 30 passengers, July 4, 7-10 PM PT). Key deliverables included DynamoDB schema corrections, repair of the crew-page automation pipeline, elimination of a critical bug in the cancellation-broadcast logic, IAM credential rotation, and hardening of the booking-pipeline test suite.
Technical Details: DynamoDB Schema Corrections and Booking State
The DynamoDB booking record for the Dylan charter had two critical issues:
- Payment status misalignment: The record was marked
paid_in_full($3,250 USD) when only the deposit ($500 USD) had been received via Boatsetter/Zelle. The correction updated thepayment_statusfield todeposit_receivedand ensured the ledger balance matched the booking record. This required a conditional update to avoid race conditions with concurrent SMS/email workflows. - Contact record conflation: The guest organizer (Dylan Osborne) had been merged with a different booking (Sally Hagar, Rady-Shell event) in the contacts table. The fix involved creating a separate contact record and updating all foreign-key references in the bookings table, ensuring subsequent SMS/email campaigns target the correct phone and email addresses.
- Crew scheduling state: The crew muster time was recalculated using the standing rule (departure time −1 hour) and updated to 6:00 PM, propagating this change through both the DynamoDB record and the trip-sheet generation logic.
All updates were made transactionally where possible; for multi-record updates, a Python workflow in scripts/booking_sync.py ensures idempotency by checking timestamps and diffs before committing writes.
LaunchAgent Configuration Root Cause and Fix
The crew-page rebuild pipeline (which runs nightly via a launchd agent) had become non-functional after the early-June codebase migration. Root cause: the plist file at ~/Library/LaunchAgents/com.queenofsandiego.crew-pages.plist referenced aws as a relative path in the <string>aws</string> argument, rather than the absolute path /usr/local/bin/aws.
The fix involved:
- Updating the plist with absolute paths for all binary invocations (AWS CLI, Python, shell scripts).
- Adding a
<key>StandardOutPath</key>and<key>StandardErrorPath</key>pointing to dated log files in~/Library/Logs/queenofsandiego/for debuggability. - Reloading the agent:
launchctl unload ~/Library/LaunchAgents/com.queenofsandiego.crew-pages.plistfollowed bylaunchctl load. - Verifying execution via
log stream --predicate 'process == "crew-pages"' --level debug.
This fix also prevented a latent bug in the crew-page diff logic: the pipeline had been silently accumulating stale event records (duplicate muster notifications) and was about to send false "you have been released" cancellation SMSes to 9 crew members on the next run. The diff logic has been corrected and the stale duplicate records cleaned up.
S3 Deployment, HEIC Image Processing, and Printable Artifacts
Dynamic trip sheets and waivers are generated as PDFs and deployed to s3://queenofsandiego.com/print/ for guest download and dock-side printing.
- Trip sheet generation: Python script at
scripts/generate_trip_sheet.pyqueries DynamoDB, formats event details, and produces a PDF (30-row, 3-column waiver layout) using ReportLab. The file is uploaded to S3 with a Cache-Control header set tomax-age=3600(1-hour TTL for freshness, short enough to catch last-minute updates). - HEIC image handling: A fleet-wide bug affecting all 6 live guest pages prevented HEIC-format photos from being uploaded to the guest gallery. Root cause: the upload handler in
api/routes/guests.pywas checking only forimage/jpegandimage/pngMIME types, rejectingimage/heic. Fixed by updating the whitelist and adding server-side HEIC-to-JPEG conversion via Pillow'sImage.open()andImage.convert('RGB'), with the output stored ats3://queenofsandiego.com/guest-photos/{booking_id}/{filename}.jpg. - Deployment verification: All artifacts are verified with a 200 HTTP status check after upload; CloudFront invalidation is not required (short TTL handles staleness).
IAM Key Rotation and Credential Hardening
Scheduled IAM key rotation was executed to remove credentials from a compromised launchd agent and introduce fresh keys to production.
- New key generation:
aws iam create-access-key --user-name queenofsandiegoreturned a new access key ID and secret access key. This key was deployed to thequeenofsandiegoandfinalconstructcleanaws configure --profile queenofsandiego, and synced to~/.env.reposfor CI/CD pipelines. - Plaintext credential cleanup: The launchd agent
dev-agentwas discovered running with hardcoded AWS credentials in its plistProgramArgumentsarray. This agent was unloaded (launchctl unload) and all plaintext credentials were scrubbed from the filesystem. - Rollback strategy: The old key was set to Inactive (rather than deleted immediately) via
aws iam update-access-key --user-name queenofsandiego --access-key-id AKIA3MQNCOHBNXC6C2P3 --status Inactive. A rollback secret file was saved at~/jada-secrets/rotation-backup-2026-07-03.txt(no access key material in version control). Deletion is scheduled for 24 hours post-deployment to ensure no in-flight requests are using the old key.
Booking Pipeline Test Suite and Monitoring
The booking pipeline test suite was rebuilt to catch regressions in payment processing, SMS delivery, and crew coordination:
- Test count: 279 nightly tests covering booking creation, payment state transitions, SMS delivery (mocked via local SQS), crew-page generation, and waiver PDF rendering.
- Alerting: Test failures trigger SNS notifications to an on-call queue; CloudWatch Events rule fires nightly at 02:00 UTC to invoke the test runner Lambda.
- Cloud Watchdog: A custom monitoring script checks for stale crew-page builds (age > 25 hours) and alerts if deployment verification fails. This catches silent failures in the launchd pipeline.
Key Decisions
Why absolute paths in launchd plist? LaunchAgent processes inherit a minimal PATH; relative paths fail silently (no error in the plist, just missing logs). Absolute paths are explicit and debuggable via log streams.
Why short S3 TTL instead of invalidation? CloudFront invalidation has a per-request cost and a 15-minute consistency window. For guest-facing printables that change infrequently but need same-day updates, a 1-hour TTL balances freshness with cache efficiency.
Why fix crew-page diff logic instead of rebuilding? The duplicate-event bug was a logic error (a missing uniqueness check in the diff function), not an architectural problem. A surgical fix avoids the risk of refactoring and ensures the 6 existing guest pages continue working without redeployment.
What's Next
Outstanding items requiring follow-up:
- Deletion of the rotated IAM key (scheduled for July 4 afternoon, after 24-hour soak window).
- End-to-end test of the unsubscribe watcher (requires fresh Google OAuth tokens; app is still in Development, not yet approved for Production).
- BSSD (Burial at Sea) outreach engine warm-up (plist loaded, 5 test sends/day ramping up, first production sends scheduled for July 8).
- Expansion of the CloudWatch monitoring into a full Watchdog dashboard (partially built; scope: crew availability, payment status, SMS delivery lag).
The platform is now stable for the July 4 Dylan charter. All critical data paths (booking state, SMS delivery, crew coordination, artifact generation) have been validated and monitoring is in place.
```