Integrating Payment Logging into a Serverless Event Management System: Lambda + CloudFront + DynamoDB
Overview: The Problem
The Ship Captain Crew tool at queenofsandiego.com/tools/shipcaptaincrew needed a new capability: crew members required the ability to log payments from patrons directly within the event management interface. This required coordinating changes across three layers: the AWS Lambda backend, the CloudFront distribution, and the S3-hosted dispatch HTML SPA.
Additionally, reconnaissance revealed a routing bug where CloudFront was incorrectly sending requests for /g/*/waiver endpoints to S3 instead of the Lambda function, causing the SPA to misparse waivers as event IDs and attempt to fetch them as API resources.
What Was Done
1. Synced Stale Local State
The local copy of the dispatch HTML at /Users/cb/Documents/repos/sites/queenofsandiego.com/tools/shipcaptaincrew/index.html was 976 lines, while the S3 version was 2463 lines. Following the rule that S3 is always the source of truth in production, we pulled the current version from S3 before making any modifications. This prevented accidentally reverting recent changes.
2. Added Gmail Helper Functions to Lambda
The Lambda function at lambda_function.py needed the ability to send payment confirmation emails via AWS SES (Simple Email Service). We inserted helper functions before the main lambda_handler to construct and send notification emails when a payment is logged. These helpers:
- Built email templates with event details and payment amounts
- Invoked SES using existing AWS credentials from the Lambda execution role
- Gracefully handled email delivery failures without blocking the API response
- Leveraged existing environment variables for Gmail sender credentials
3. Implemented Payment Logging Handler in Lambda
A new route handler was added to the Lambda routing logic (inside the lambda_handler function) to accept POST requests to a new endpoint. The handler:
- Validated admin authentication via the existing auth check against
ADMIN_PASS_HASH - Accepted a JSON payload containing event ID, patron name, and payment amount
- Wrote a new item to the DynamoDB events table with a
payment_loggedtimestamp - Updated the event's total payment tracking fields
- Sent an SES notification email to crew members
- Returned a 200 response with the updated event data
This pattern follows the existing handler structure used by handle_waiver_get (line 1697) and other endpoints.
4. Added "Log Payment" Modal to Dispatch HTML
The dispatch SPA required a new UI component. We added a modal dialog following the existing modal structure pattern already in the codebase. The modal:
- Appears as an overlay when crew members click a "Log Payment" button on an event card
- Collects patron name and amount via form inputs
- Uses the existing
apiFetchhelper to POST to the new Lambda endpoint - Displays success/error messages inline
- Refreshes the event view after successful submission
- Uses the existing CSS class pattern (
activeclass for visibility) to avoid breaking the stylesheet
Infrastructure Changes
CloudFront Routing Fix
The CloudFront distribution (ID: queenofsandiego.com prod distribution) had a gap in its routing configuration. Requests to /g/*/waiver were falling through to the default S3 origin instead of being routed to the Lambda Function URL. We added a new CloudFront behavior:
- Path Pattern:
/g/*/waiver - Origin: Lambda Function URL (shipcaptaincrew)
- Viewer Protocol Policy: HTTPS only
- Cache Policy: Disabled (Lambda responses are dynamic)
- Origin Request Policy: AllViewerAndCloudFrontHeaders
This ensures waiver requests reach the correct handler at line 1697 in lambda_function.py rather than being misinterpreted by the SPA.
Lambda Environment Variables
The Lambda function's environment variables were merged to include Gmail sender credentials (stored via AWS Secrets Manager references, not plaintext). The deployment captured the current env state, verified the merge didn't lose existing vars like ADMIN_PASS_HASH, and pushed the combined payload before deploying new code.
Deployment flow:
1. Build deployment zip from local lambda_function.py + requirements
2. Create merged env vars payload (existing + new)
3. Update Lambda config (SetFunctionConfiguration) with env vars
4. Wait for config update to settle (describe-function-configuration loop)
5. Update Lambda code (UpdateFunctionCode) with new zip
6. Wait for code update to settle
7. Invalidate CloudFront cache for staging slot
S3 and DynamoDB
The DynamoDB table schema remained unchanged; new payment fields were added as optional attributes on existing event items. The S3 bucket (shipcaptaincrew root) hosted the updated dispatch HTML in both prod and the staging slot. The staging slot allowed testing before production rollout.
Key Technical Decisions
- Why SES for email? Lambda's execution role already had SES permissions; no additional IAM setup was needed. SES integrates natively with AWS infrastructure.
- Why validate against ADMIN_PASS_HASH? The existing admin login handler already used this pattern; reusing it avoided adding new auth mechanisms and kept secrets management consistent.
- Why CloudFront behavior for /g/*/waiver? The SPA can't handle waiver HTML responses. Routing at the edge ensures the right backend answers each request type, reducing wasted round trips and confused error handling in browser JavaScript.
- Why merge env vars before code deploy? If new code depends on new env vars being present, deploying code first risks runtime NameErrors. The merge step ensures config + code arrive together.
Testing and Validation
Before production deployment, the staging slot at /_staging/ was updated and tested. Admin login was verified against the new password hash, and the payment endpoint was probed to confirm it was reachable (401 = authentication required, 404 = route missing). After validating the staging behavior, the prod dispatch HTML was updated and CloudFront cache was invalidated for the distribution.
What's Next
Once crew members log in and the payment logging feature is live, confirm via CloudWatch Logs that:
- Payment POST requests are reaching the new handler (check Lambda logs for handler invocations)
- DynamoDB writes are succeeding (check payment_logged timestamps in event items)
- SES emails are being sent and delivered (check SES send statistics in CloudWatch)
- Waiver requests are now routed correctly (check CloudFront access logs for /g/*/waiver requests hitting Lambda, not S3)