Integrating Gmail Token Retention and Payment Logging into Queen of San Diego's Ship Captain Crew Tool
This post documents a significant infrastructure and application update to the Ship Captain Crew (SCC) tool, focusing on two key objectives: preserving Gmail credentials across Lambda deployments and adding payment logging capability for event patrons. The work involved coordinating changes across AWS Lambda, CloudFront, DynamoDB, S3, and the browser-side dispatch SPA.
Problem Statement
The Ship Captain Crew tool—a crew management interface for Queen of San Diego's tall ship events—needed two capabilities:
- Gmail credential persistence: Environment variables storing Gmail OAuth tokens were being lost during Lambda code deployments, requiring manual re-entry.
- Payment logging: Event crew needed a way to log patron payments directly in the SCC interface without leaving the app.
Additionally, a routing bug was identified where CloudFront was incorrectly directing waiver requests (/g/{event_id}/waiver) to S3, causing the SPA to misparse date slugs as event IDs.
Architecture Overview
The SCC tool is a three-tier system:
- Frontend: Static SPA dispatch HTML served via S3 and CloudFront from
s3://queenofsandiego-shipcaptaincrew/dispatch/index.html - Compute: AWS Lambda function at
/Users/cb/Documents/repos/sites/queenofsandiego.com/tools/shipcaptaincrew/lambda_function.py, invoked via CloudFront function URL - Data: DynamoDB table storing event records with schema including crew assignments, payments, and waivers
The Lambda function also integrates with AWS SES for email notifications and requires Gmail credentials for email functionality.
Technical Implementation
1. Gmail Token Retention Strategy
The core issue: during Lambda deployments, the deployment script rebuilds the function's environment variables but didn't preserve existing Gmail-related secrets. Solution involved three steps:
- Snapshot production state: Before any deployment, capture the current Lambda configuration and environment variables using AWS CLI
- Merge credentials into deployment payload: Read production Gmail env vars (e.g.,
GMAIL_SENDER,GMAIL_TOKEN, related OAuth fields) and merge them with the new deployment's environment variables - Deploy atomically: Push the merged payload to Lambda, then deploy the code zip separately
Example workflow (credentials redacted):
# 1. Snapshot production Lambda config
aws lambda get-function-configuration \
--function-name shipcaptaincrew-handler \
--region us-west-2 > prod_config.json
# 2. Extract Gmail env vars from snapshot
grep -E "GMAIL_|OAUTH_" prod_config.json > gmail_vars.txt
# 3. Merge into new env vars payload and deploy
aws lambda update-function-configuration \
--function-name shipcaptaincrew-handler \
--environment Variables={existing_vars + gmail_vars}
# 4. Wait for config update to settle
sleep 10
# 5. Deploy code separately
aws lambda update-function-code \
--function-name shipcaptaincrew-handler \
--zip-file fileb://lambda_function.zip
This pattern prevents credential loss and follows AWS best practice of separating config from code deployments.
2. Payment Logging Handler in Lambda
Added a new handler function handle_payment_log() in lambda_function.py (inserted before the main lambda_handler) to process payment submissions:
def handle_payment_log(event, path_parts):
"""
POST /api/g/{event_id}/payment-log
Body: {
"patron_name": "John Doe",
"amount": 150.00,
"payment_method": "cash|check|card",
"notes": "optional notes"
}
Returns: updated event record with payment appended to payments array
"""
# Validation, auth check (same pattern as other handlers)
# Write to DynamoDB payments field
# Return updated event
The handler follows the existing routing pattern in lambda_handler, checking authentication via the check_admin_auth() helper and validating the event exists in DynamoDB before recording the payment.
3. Frontend Modal UI in Dispatch HTML
Added a "Log Payment" modal to /Users/cb/Documents/repos/sites/queenofsandiego.com/tools/shipcaptaincrew/index.html following existing modal patterns in the codebase:
- Modal HTML: Inserted a form with fields for patron name, amount, payment method (radio buttons), and optional notes
- Modal display: Used the existing
activeCSS class pattern (matching the "Add Crew" and other modals) rather than inlinestyleattributes - JavaScript handler: Added
openPaymentModal()andsubmitPaymentLog()functions that use the existingapiFetch()helper to POST to/api/g/{event_id}/payment-log
The modal integrates into the event card's context menu, appearing alongside existing options.
4. CloudFront Waiver Routing Fix
Diagnosed: CloudFront was routing /g/*/waiver to S3 instead of the Lambda function, causing the SPA to receive HTML instead of JSON from a misrouted API call.
Fix: Added a new CloudFront behavior for the distribution (ID not published, but resides in prod account):
Behavior Path: /g/*/waiver
Target Origin: shipcaptaincrew Lambda Function URL
Compress: Yes
Cache Policy: No cache (Lambda responses vary per auth)
This ensures waiver requests reach the existing handle_waiver_get() handler at line 1697 of lambda_function.py, which returns proper HTML for waiver forms.
Infrastructure Changes Summary
| Resource | Change | Reason |
|---|---|---|
Lambda: shipcaptaincrew-handler |
Added handle_payment_log(), updated lambda_handler routing, merged Gmail env vars |
Support payment logging; retain credentials across deployments |
S3: queenofsandiego-shipcaptaincrew |
Updated dispatch/index.html with payment modal + JS handlers |
User interface for payment logging |
| CloudFront Distribution | Added behavior: /g/*/waiver → Lambda origin |
Route waiver requests to correct Lambda handler, not S3 SPA fallback |