```html

Deploying a Multi-Tenant Event Management Platform: Lambda, S3, and Real-Time State Management for Charter Operations

Over the past development session, we deployed a complete event management system for QueenofSanDiego's charter operations, integrating AWS Lambda serverless compute, S3-based frontend hosting, CloudFront CDN distribution, and DynamoDB for persistent state. This post walks through the architecture decisions, deployment patterns, and infrastructure configuration that enables real-time checklist synchronization, role-based access control, and event lifecycle management across multiple charter types.

What Was Done

We built and deployed three core components:

  • Lambda function (`/Users/cb/Documents/repos/sites/queenofsandiego.com/tools/shipcaptaincrew/lambda_function.py`) — RESTful event handler with JWT authentication, event CRUD operations, checklist state management, and role claim workflows
  • Frontend SPA (`/Users/cb/Documents/repos/sites/queenofsandiego.com/tools/shipcaptaincrew/frontend/index.html`) — Single-page application serving interactive event dashboards, checklist panels, and real-time role designation UI
  • Infrastructure automation — EventBridge cron rules, DynamoDB table configuration, SES email triggers, and CloudFront cache invalidation pipelines

The system handles charter event creation, crew role assignment (captain, first mate, deckhand), waiver workflows, and departure/return timing synchronization. Magic link authentication enables passwordless invite-based access, with JWT tokens scoped to user identity and role.

Technical Architecture

Lambda Function Design

The Lambda function serves as the single API endpoint for all event operations. It's organized around a request router that dispatches based on HTTP method and path:


POST /events                    → Create new event with initial crew assignments
GET /events/{event_id}          → Fetch event details + checklist state
PUT /events/{event_id}          → Update event metadata (departure time, return time, etc.)
POST /events/{event_id}/checklists → Initialize checklist from template
PUT /events/{event_id}/checklists/{checklist_id} → Update checklist item state
POST /events/{event_id}/roles/{role}/claim → Crew member claims a role
POST /events/{event_id}/roles/{role}/release → Crew member releases assigned role
GET /events/{event_id}/guests   → List all crew on event (waiver status, roles)

Key handlers within the Lambda codebase:

  • authenticate_request() — Extracts and validates JWT from Authorization header; rejects unauthenticated requests with 401
  • create_event(body, user_id) — Validates required fields (vessel_id, event_date, captain_id), generates event_id, stores in DynamoDB with initial role assignments
  • load_checklist(event_id, checklist_template_id) — Fetches checklist template from DynamoDB, instantiates items with pending state, binds to event
  • claim_role(event_id, role, user_id) — Transitions role state from unassigned → claimed, updates crew roster, sends confirmation email via SES
  • get_sunset_time(event_date) — Calls local timeserver API to compute sunset, used for departure/return timing validation

Environment variables configure:

  • DYNAMODB_EVENTS_TABLE — Table name for event documents
  • DYNAMODB_CHECKLISTS_TABLE — Table name for checklist templates and instances
  • SES_SENDER_EMAIL — Verified SES sender address (hardcoded to `admin@queenofsandiego.com`)
  • JWT_SECRET — Symmetric key for HS256 token validation (stored in AWS Secrets Manager)
  • FRONTEND_URL — Base URL for magic link generation

Frontend State Management

The SPA (`index.html`) implements a lightweight state machine for event lifecycle visibility:

  • Event view — Displays crew roster with role badges, checklist progress bars, and timing panels (departure/return countdown)
  • Checklist panel — Interactive checklist items with tri-state toggles (pending, in-progress, complete); real-time sync via AJAX
  • Role claim modal — Allows authenticated crew to self-designate for unassigned roles; triggers Lambda claim handler
  • Waiver modal — Renders waiver HTML from SES template, captures e-signature, POSTs acknowledgment to Lambda

JavaScript hooks into timing functions via onDeparture() and onReturn() callbacks, triggering state transitions in the backend when elapsed time matches computed sunset window.

Infrastructure & Deployment

S3 & CloudFront Configuration

Frontend assets are deployed to an S3 bucket configured for static website hosting:


S3 Bucket: queenofsandiego-shipcaptaincrew-frontend
Region: us-west-2
Versioning: Enabled
Block Public Access: All disabled (required for CloudFront public read)

CloudFront distribution handles edge caching and HTTPS termination:


CloudFront Distribution ID: E2ABCD1234567 (example)
Origin: S3 bucket website endpoint
Cache Behavior:
  - Default TTL: 300 seconds (5 minutes) for index.html
  - Max TTL: 86400 seconds (1 day) for versioned assets
  - Compress: Enabled (gzip for HTML, CSS, JS)

After each frontend deployment, we invalidate the CloudFront cache for /index.html and /* to ensure fresh content:


aws cloudfront create-invalidation \
  --distribution-id E2ABCD1234567 \
  --paths "/index.html" "/*"

Lambda Deployment & Versioning

The Lambda function is zipped and deployed via the AWS CLI with alias management for safe rollback:


cd /Users/cb/Documents/repos/sites/queenofsandiego.com/tools/shipcaptaincrew
zip -r lambda_deployment.zip lambda_function.py
aws lambda update-function-code \
  --function-name shipcaptaincrew-api \
  --zip-file fileb://lambda_deployment.zip
aws lambda publish-version --function-name shipcaptaincrew-api
aws lambda update-alias \
  --function-name shipcaptaincrew-api \
  --name production \
  --routing-config AdditionalVersionWeight=10 \
  --function-version 42

Before deployment, syntax validation is performed to catch import errors and undefined references:


python3 -m py_compile lambda_function.py && echo "Syntax OK"

EventBridge Automation

Scheduled cron rules trigger reminder emails and state transitions:


Rule: ptb_nudge
Schedule: cron(0 18 * * ? *)  #