```html

Building the Real-Time Guest Photo Gallery: Architecture & Implementation of Keely's Sailor Board

What Was Done

Over this session, we completed the full implementation of Keely's guest event page at https://queenofsandiego.com/g/2026-05-24-keely-afternoon, transforming a concept into a working photo upload and gallery system. The page now handles real-time photo/video uploads from guests with spam protection via event codes, automatic Instagram hashtag integration, and moderator workflows.

The "Sailor Board" — initially just flavor text in Lambda response messages — is now an actual, functional per-event photo gallery that aggregates guest uploads and curated Instagram posts all in one place.

Technical Architecture

Frontend: Guest Upload Flow

The guest-facing interface lives in two places:

  • /sites/queenofsandiego.com/g/index.html — template (regenerated per event)
  • /sites/queenofsandiego.com/index.html — homepage with nav linking to active events

The upload mechanism uses a standard multi-file input with client-side validation:

<input type="file" id="file-input" 
  accept="image/jpeg,image/png,image/webp,image/heic,image/heif,video/mp4,video/quicktime,video/x-msvideo,video/webm"
  multiple />

The handleFiles() function (line 514 in the deployed Keely page) enforces a hard cap of 24 files per submission and batches them through uploadFile() calls. Each upload:

  1. Calls GET /api/presign (Lambda endpoint) to obtain a signed S3 URL
  2. POSTs the file directly to S3 (browser-side, not through our servers)
  3. Triggers a DynamoDB entry via Lambda for tracking

Why direct-to-S3 uploads? Reduces Lambda invocations, avoids multipart body parsing overhead, and lets CloudFront caching accelerate gallery rendering without re-hitting Lambda on every page load.

Event Code Spam Gate

Guests can optionally enter an event code (input #g-code, line 354) before uploading. The code is validated server-side in the Lambda's readCode() handler (line 424):

  • Code provided + matches event: Photos go straight to gallery (status: 'approved' in DynamoDB)
  • No code provided: Photos go to moderation queue (status: 'pending'); moderators review via email before publishing

Codes are scoped per event and stored in a separate DynamoDB table (event-codes). The moderator email (sent by Lambda at line 2066) includes a "View sailor board" link that points back to the same guest page — the guest sees their own photos in context.

Instagram Hashtag Integration

After the event end time (defined by FLIP_UTC, e.g., May 25 00:00 UTC for Keely's May 24 afternoon sail), the page switches from upload mode to gallery mode. The Lambda endpoint GET /api/g/{event_id}/photos returns a JSON response that includes:

{
  "photos": [...],
  "instagram": [...]  // Posts from #jada, #queenofsandiego, same-day only
}

The Instagram fetch is wired into tools/shipcaptaincrew/lambda_function.py around line 1900+. The handler:

  1. Parses the event date from the URL parameter (2026-05-24)
  2. Calls the Instagram Graph API (via credentials stored in Lambda environment) with hashtag filters
  3. Applies a date window: only posts from midnight–midnight of the event date
  4. Caches the result in DynamoDB to avoid repeated API calls

On the frontend, the Instagram posts render into #ig-grid (lines 452–459 of the guest page) using the same card layout as uploaded photos, creating a seamless mixed gallery.

Infrastructure & Deployment

Lambda Function

Function name: shipcaptaincrew

Runtime: Python 3.11

Handler: lambda_function.lambda_handler

Timeout: 30 seconds

Memory: 512 MB

During this session, the function was updated multiple times (files show 9+ edits to lambda_function.py). Each change was:

  1. Tested locally for syntax: python -m py_compile lambda_function.py
  2. Packaged with dependencies: zip -r lambda_deploy.zip . (includes bundled py_vapid for web push notifications)
  3. Deployed via: aws lambda update-function-code --function-name shipcaptaincrew --zip-file fileb://lambda_deploy.zip

The function is aliased to prod via API Gateway, with the endpoint:

https://shipcaptaincrew.queenofsandiego.com/api/...

S3 & CORS Configuration

Bucket: queenofsandiego-photos-prod (guest uploads)

Bucket: queenofsandiego-site-prod (HTML/JS assets)

CORS was updated to allow cross-origin PUT requests from queenofsandiego.com origins (necessary for browser-side uploads):

[
  {
    "AllowedHeaders": ["*"],
    "AllowedMethods": ["GET", "PUT"],
    "AllowedOrigins": ["https://queenofsandiego.com"],
    "MaxAgeSeconds": 3000
  }
]

CloudFront & Content Delivery

Staging distribution: Used for pre-production validation

Production distribution: Serves queenofsandiego.com and shipcaptaincrew.queenofsandiego.com

After each code deploy, CloudFront cache was invalidated:

aws cloudfront create-invalidation --distribution-id <DIST_ID> --paths "/*"

This ensures guests see the latest page markup and JavaScript immediately.

Key Decisions & Trade-Offs

Per-Event Pages vs. Centralized Board

We chose per-event guest pages rather than a single aggregate "sailor board" because:

  • Events are private by default; guests should only see photos from their