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 active CSS class pattern (matching the "Add Crew" and other modals) rather than inline style attributes
  • JavaScript handler: Added openPaymentModal() and submitPaymentLog() functions that use the existing apiFetch() 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