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:
- Calls
GET /api/presign(Lambda endpoint) to obtain a signed S3 URL - POSTs the file directly to S3 (browser-side, not through our servers)
- 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:
- Parses the event date from the URL parameter (
2026-05-24) - Calls the Instagram Graph API (via credentials stored in Lambda environment) with hashtag filters
- Applies a date window: only posts from midnight–midnight of the event date
- 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:
- Tested locally for syntax:
python -m py_compile lambda_function.py - Packaged with dependencies:
zip -r lambda_deploy.zip .(includes bundledpy_vapidfor web push notifications) - 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