```html

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.html with 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, and GMAIL_SENDER_EMAIL from 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_HASH environment variable
  • Extracts event_id, patron_name, and amount from the request body
  • Updates the target event record in DynamoDB, setting payment_cleared: true and 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