Building a Payment Logging System for Event Crew: Lambda Handlers, DynamoDB Schema Integration, and CloudFront Routing Fixes
This session focused on implementing a payment logging feature for the Ship Captain Crew event management tool while fixing routing issues that prevented waiver pages from loading correctly. The work involved adding Gmail credential support to Lambda, creating new handler functions for payment state management, updating the dispatch SPA with a modal interface, and correcting CloudFront behavior rules to route waiver requests to the correct origin.
Problem Statement
The ShipCaptainCrew tool needed a way for admins to log patron payments without modifying event data directly. Additionally, waiver requests to paths like /g/2026-05-23/waiver were incorrectly routing to S3 (which returned the dispatch HTML), causing the SPA to attempt parsing the date slug as an event ID and fail with "Could not load event."
Architecture Overview
The ShipCaptainCrew infrastructure consists of:
- Lambda Function URL:
https://[lambda-url]/shipcaptaincrew— handles API requests and server-side rendering - S3 Origin:
s3://queenofsandiego-shipcaptaincrew/— hosts the dispatch SPA and static assets - CloudFront Distribution: Routes requests based on path patterns to either Lambda or S3
- DynamoDB Table: Stores event metadata, crew assignments, and waivers
- Environment Variables: Lambda reads Gmail credentials, admin password hash, and feature flags from env config
Key Changes: Lambda Payment Handlers
The Lambda function (/Users/cb/Documents/repos/sites/queenofsandiego.com/tools/shipcaptaincrew/lambda_function.py) was extended with two new helper functions and a handler before the main lambda_handler routing logic:
def send_payment_confirmation_email(patron_name, event_id, amount):
"""
Uses AWS SES to send a payment receipt email.
Requires: SES_SENDER_EMAIL env var, Gmail SMTP credentials in env.
"""
def handle_payment_log(event):
"""
POST /api/g/{event_id}/payment
Validates admin auth token, logs payment entry to DynamoDB events table,
returns confirmation with timestamp and transaction reference.
"""
Why these functions: By centralizing email delivery and payment logic, we avoid code duplication and make it easier to add audit logging or Stripe integration later. The handler follows the same pattern as existing routes like handle_waiver_get (line 1697), making the codebase predictable for future maintainers.
The payment handler validates the incoming request against the admin password hash (stored as ADMIN_PASS_HASH in Lambda environment variables) before writing. This prevents unauthorized payment logging while avoiding plaintext credentials in code.
Dispatch HTML: Payment Logging Modal
The dispatch SPA (/Users/cb/Documents/repos/sites/queenofsandiego.com/tools/shipcaptaincrew/index.html) required a modal interface to capture payment information. The implementation:
- Added modal HTML structure with form fields for patron name, amount, and payment method
- Integrated with existing
apiFetchhelper to POST to/api/g/{event_id}/payment - Used existing modal display pattern (checking for
activeclass) to avoid CSS conflicts - Added validation to ensure amount is positive and patron name is not empty
The modal triggers from the event card UI (rendered by the event card function around line 1200) via a "Log Payment" button. On successful submission, the modal closes and the event card refreshes to reflect the new payment entry in the payments array returned by handle_list_events.
Infrastructure: CloudFront Routing Fix
The waiver routing issue required a new CloudFront behavior rule. The current setup was:
Behavior 1: /api/* → Lambda Function URL
Behavior 2: /* → S3 Origin (fallback)
Requests to /g/2026-05-23/waiver matched Behavior 2 and went to S3, which served the dispatch HTML. The fix adds:
Behavior 0: /g/*/waiver → Lambda Function URL (highest priority)
Behavior 1: /api/* → Lambda Function URL
Behavior 2: /* → S3 Origin (fallback)
This ensures waiver requests route to Lambda's handle_waiver_get (line 1697 of lambda_function.py) which returns HTML directly, rather than relying on the SPA to parse the URL.
Cache settings: The new behavior uses a short TTL (60 seconds) since waiver HTML is dynamic and may update when crew signs or event details change.
Environment Variables and Secrets Management
Lambda now reads Gmail SMTP credentials from environment variables to support email receipts. The deployment process used a merged env vars payload that preserved existing credentials while adding new ones:
Variables:
SES_SENDER_EMAIL=crew@queenofsandiego.com
ADMIN_PASS_HASH=[bcrypt hash of admin password]
GMAIL_SMTP_HOST=smtp.gmail.com
GMAIL_SMTP_PORT=587
GMAIL_SENDER_EMAIL=[provisioned service account]
GMAIL_APP_PASSWORD=[app-specific password, not user password]
The app password (distinct from the user's Google account password) is generated via Google Cloud Console and rotated independently. This reduces blast radius if credentials leak.
Deployment Flow
The deployment followed this sequence:
- Backup: Snapshot prod Lambda zip and HTML before any changes
- Local edits: Update lambda_function.py and index.html
- Syntax check: Run Python syntax validator on lambda_function.py to catch typos
- Build zip: Create deployment zip of lambda_function.py
- Merge env vars: Combine new credentials with existing env config; verify Gmail fields present
- Deploy Lambda config: Push merged env vars to Lambda; wait for config update to settle (~3 seconds)
- Deploy Lambda code: Push updated zip; wait for code update (~5 seconds)
- Deploy HTML to staging: Push updated index.html to S3 staging slot
- Invalidate cache: CloudFront invalidation for
/_staging/*to force edge refresh - Smoke test: Verify staging URL serves new HTML; test admin login; probe new endpoints
- Verify routing: Confirm
/g/*/waiverreturns 200 (Lambda) not 404 (S3)
Key Design Decisions
- Payment handler before lambda_handler routing: Keeps related logic grouped; easier to add audit hooks or transaction IDs later
- Email via SES, not Gmail SMTP: SES is more reliable for high-volume notifications