Building a Payment Logging System for Event Management: Lambda, DynamoDB, and SPA Integration
This post documents the design and implementation of a payment logging feature for the Ship Captain Crew event management system. The work involved integrating Gmail credential handling into a Lambda function, adding payment state management to a Single Page Application (SPA), and ensuring proper CloudFront routing for hybrid server-rendered and client-side content.
Problem Statement
The Ship Captain Crew tool (hosted at queenofsandiego.com/tools/shipcaptaincrew/) needed the ability to log payments for patrons attending charter events. The system had:
- A DynamoDB table storing event data with patron rosters
- A Lambda function handling API requests via CloudFront Function URL routing
- A React-style SPA dispatch HTML serving as the primary UI
- Partial payment infrastructure (banner, modal structure, auth checks) already in place
The gap: no handler to accept, validate, and persist payment records, and no UI component to trigger that flow.
Architecture Overview
The Ship Captain Crew stack consists of:
- Lambda:
/Users/cb/Documents/repos/sites/queenofsandiego.com/tools/shipcaptaincrew/lambda_function.py— routes HTTP requests, validates auth via ADMIN_PASS_HASH, queries/updates DynamoDB - DynamoDB: Schema includes event items with partition key
event_id(format:YYYY-MM-DD-firstname-[period]) and nested patron rosters - SPA Dispatch HTML:
queenofsandiego.com/tools/shipcaptaincrew/index.html(pulled from S3, currently 2463 lines after sync) — client-side routing, event fetching, UI rendering - CloudFront Distribution: Routes
/api/*requests to Lambda Function URL; servesindex.htmlfor SPA routes - S3 Bucket:
shipcaptaincrew— stores dispatch HTML and static assets
Technical Implementation
Lambda Enhancement: Payment Handler
The Lambda function already had helper functions for:
- Admin authentication via ADMIN_PASS_HASH comparison
- SES email dispatch
- DynamoDB queries with error handling
We added a new payment handler following the existing routing pattern:
def handle_payment_log(event, context):
"""
POST /api/events/{eid}/patron/{pid}/payment
Logs a payment for a patron, updates DynamoDB,
and sends confirmation email.
"""
# 1. Extract and validate path parameters
# 2. Verify admin auth from headers
# 3. Parse JSON body (amount, method, notes)
# 4. Update patron payment_cleared field in DynamoDB
# 5. Send SES confirmation to admin
# 6. Return 200 with updated patron record
This handler was inserted before the main lambda_handler routing dispatcher (around line 1750 in the development version), following the same pattern as existing handlers like handle_get_event and handle_list_events.
Routing and Path Parsing
The Lambda router uses regex matching on the request path:
path = event['rawPath'] # e.g., /api/events/2026-05-23-john/patron/john-smith/payment
if path.startswith('/api/events/') and path.endswith('/payment'):
return handle_payment_log(event, context)
This pattern avoids conflicts with existing routes and maintains consistency with the codebase.
SPA Frontend: Payment Modal
The dispatch HTML already had modal infrastructure (active class toggling). We added a "Log Payment" modal following the same pattern:
- Modal trigger: Button in patron card row, visible only to authenticated admins
- Form fields: Amount (float), payment method (dropdown: cash/check/venmo/stripe), optional notes
- Submission: Uses existing
apiFetchhelper to POST to/api/events/{eid}/patron/{pid}/payment - Error handling: Catch block displays user-friendly error (e.g., "Could not log payment: network error")
- Success handling: Refresh event data, close modal, show success banner
The modal HTML was inserted after the existing event-detail modal, around line 1400 in the dispatch HTML.
Infrastructure and Deployment
Environment Variables
The Lambda function environment includes credentials for SES and Gmail token refresh (if applicable). We verified these were present before deploying:
aws lambda get-function-configuration \
--function-name shipcaptaincrew \
--query 'Environment.Variables' \
--region us-west-2
The merged env payload was pushed to preserve existing variables while adding/updating payment-related config.
Deployment Workflow
- Lambda Code: Snapshot prod zip, apply patches locally, syntax-check Python, build new zip, deploy via AWS API
- Lambda Config: Merge env vars, push to Lambda, wait for config update to settle (typically <5 seconds)
- SPA HTML: Sync from S3 (to catch any upstream changes), apply patches, deploy to S3, invalidate CloudFront cache at
/* - Staging validation: Deploy to
/_staging/path, test with real admin login, verify API responses before production rollout
CloudFront Routing (Known Issue)
During development, we discovered a routing gap: requests to /g/{event_id}/waiver were falling through to the SPA instead of reaching the Lambda waiver handler. The fix (not yet implemented) requires adding a new CloudFront behavior:
- Path pattern:
/g/*/waiver - Origin: Lambda Function URL
- Priority: Higher than the default SPA catch-all
This is a broader infrastructure cleanup task tracked separately.
Key Decisions
- Handler placement in Lambda: Inserted before
lambda_handlerto follow the existing pattern and keep related logic grouped - Modal pattern reuse: Used existing HTML modal structure and CSS classes to minimize new code and maintain consistency
- Admin-only visibility: Payment logging is gated by auth headers and frontend UI visibility checks, preventing unauthorized access
- Staging slot testing: Deployed to
/_staging/path to validate changes before production, using real admin credentials for smoke tests - SES for confirmation: