Building a Payment Logging System for Event Management: Lambda, DynamoDB, and CloudFront Integration
What Was Done
We implemented a complete payment logging workflow for the Ship Captain Crew event management tool, integrating Gmail credential handling, payment state tracking in DynamoDB, a new admin-authenticated payment logging endpoint in AWS Lambda, and a modal UI component in the dispatch HTML. This work addresses the operational need to record patron payments outside the automated booking system while maintaining audit trails and role-based access control.
Technical Architecture Overview
The system follows a three-tier architecture:
- Frontend: Modal UI in
/Users/cb/Documents/repos/sites/queenofsandiego.com/tools/shipcaptaincrew/index.htmlwith payment form and API client - Compute: AWS Lambda function (
lambda_function.py) handling HTTP routing, admin authentication, and payment persistence - Data Layer: DynamoDB table storing event records with payment_cleared field, CloudFront distribution caching responses
Lambda Function Architecture Changes
The Lambda function required two major additions:
Gmail Credential Management
We integrated Gmail token handling by:
- Reading environment variables
GMAIL_SERVICE_ACCOUNT_JSON,GMAIL_TOKEN, andGMAIL_SENDER_EMAILfrom Lambda configuration - Adding helper functions to parse service account credentials and refresh OAuth tokens as needed
- Storing these credentials in Lambda environment rather than hardcoding, following AWS secrets management best practices
This design allows the ops team to rotate credentials by updating Lambda environment variables without code changes, and the staging/production distinction remains automatic based on deployment slot.
Payment Handler Implementation
Added a new handler handle_log_payment in lambda_function.py that:
- Accepts POST requests to
/api/log-payment - Requires admin authentication via X-Admin-Token header, validated against
ADMIN_PASS_HASHenvironment variable - Extracts event_id, patron_name, and amount from the request body
- Updates the target event record in DynamoDB, setting
payment_cleared: trueand recording the payment timestamp - Returns the updated event object to the client for immediate UI reflection
The handler follows the existing pattern in the codebase: parse input → authenticate → query DynamoDB → return JSON response or 400/401/500 error.
DynamoDB Schema Integration
The existing events table (structure queried via describe_table call) already has partition key event_id and sort key timestamp. We added/verified the payment_cleared field as a boolean attribute:
{
"event_id": "2026-05-23-alice-period",
"timestamp": 1234567890,
"payment_cleared": true,
"payment_cleared_at": "2024-01-15T14:22:30Z",
"payment_amount": 450.00
}
This denormalization is intentional: payment state must be queryable during the handle_list_events call so the crew dashboard shows which events have received payment without additional round-trips.
Frontend: Modal UI and API Integration
The dispatch HTML received a new modal component following the existing pattern in the codebase. Modal structure uses class="active" for visibility (consistent with existing modals like booking confirmation), not inline style manipulation.
The modal includes:
- Text input for event_id (auto-populated from dashboard context if available)
- Text input for patron name
- Number input for payment amount
- Hidden input for admin token (populated from localStorage after successful admin login)
- Submit button calling
logPayment()JavaScript function
The logPayment() function uses the existing apiFetch() helper, which manages the X-Admin-Token header and error handling:
const logPayment = async () => {
const eventId = document.getElementById('payment-event-id').value;
const patronName = document.getElementById('payment-patron-name').value;
const amount = parseFloat(document.getElementById('payment-amount').value);
const response = await apiFetch('/api/log-payment', {
method: 'POST',
body: JSON.stringify({ event_id: eventId, patron_name: patronName, amount })
});
if (response.ok) {
showBanner('Payment logged successfully', 'success');
closePaymentModal();
refreshEventsList();
}
};
Infrastructure and Deployment Strategy
CloudFront and S3 Configuration
The system uses CloudFront distribution serving S3 bucket queenofsandiego.com with two slot strategy:
- Staging:
/_staging/*behavior routes to Lambda Function URL for testing new handlers before production - Production: Root path behavior routes to S3 dispatch HTML; separate behaviors for
/api/*routes to Lambda
The /api/log-payment route is wired into the existing /api/* Lambda behavior, so it automatically inherits the CloudFront cache policy and regional edge caching.
Environment variable synchronization: before deploying code changes to Lambda, we constructed a merged env vars payload combining existing credentials (GMAIL_SERVICE_ACCOUNT_JSON, etc.) with new route handlers, then pushed via AWS CLI:
aws lambda update-function-configuration \
--function-name shipcaptaincrew \
--environment Variables={KEY1=value1,KEY2=value2,...}
We waited for the LastUpdateStatus: Successful status before deploying code, ensuring no race conditions between env setup and code execution.
S3 Dispatch HTML Deployment
Updated HTML was built locally, compared against S3 current (2463 lines vs. local 976 line stale copy), refreshed from S3, modified with new modal, and redeployed to staging slot first:
aws s3 cp index.html s3://queenofsandiego.com/_staging/tools/shipcaptaincrew/
aws cloudfront create-invalidation --distribution-id [DIST_ID] --paths "/_staging/*"
After staging validation, the same HTML was deployed to production with CloudFront cache invalidation, ensuring old cached versions expire within seconds.
Key Architectural Decisions
Admin Token in localStorage: The payment modal retrieves the admin token from browser localStorage (set during admin login flow), avoiding the need to re-authenticate for each payment entry while keeping credentials client-side (not in HTML source).
Denormalized payment_cleared field: Rather than storing payments in a separate DynamoDB table, we updated the event record directly, prioritizing query simplicity in handle_list_events over strict normalization.
Email notification as future task