Building Real-Time Photo Gallery Infrastructure for Charter Event Pages: Keely's Guest Page Deep Dive
Overview: What We Built
During this session, we completed the technical implementation of a per-charter guest photo upload and gallery system, deployed live on Keely's afternoon charter event page at https://queenofsandiego.com/g/2026-05-24-keely-afternoon. The system handles photo/video uploads with client-side validation, server-side moderation queuing, Instagram hashtag integration, and real-time gallery rendering—all gated by an event-specific code to prevent spam.
Architecture Overview
The infrastructure uses a three-tier design:
- Frontend: Static HTML/JS guest page deployed to S3 CloudFront
- Backend: AWS Lambda handler for presigned upload URLs, photo metadata, and moderation queue
- Storage: S3 buckets for original photos + thumbnails, DynamoDB for photo metadata and moderation state
Technical Implementation Details
File Structure and Deployment Locations
The guest page source lives in the repo at /Users/cb/Documents/repos/sites/queenofsandiego.com/sailor-board/index.html, though the production copy (deployed May 25, 2026, 02:11 UTC) is served directly from S3 at the path-based URL queenofsandiego.com/g/{event_id}. The page is 22.5 KB and contains embedded JavaScript for upload handling.
Related supporting files include:
booking-widget.js— embedded booking modal that opens the Stripe checkout flow (with fixes applied this session to prevent homepage redirect)/tools/shipcaptaincrew/lambda_function.py— the main API handler for photo presigning, metadata retrieval, and moderation workflowpulse/index.htmland dated pulse posts (e.g.,2026-05-25-yamato-class-battleship/index.html) — daily naval history feature feed
Photo Upload Flow
The upload system uses presigned POST URLs to keep credentials off the client. The guest page implements this workflow:
- Client validation: The page accepts JPEG, PNG, WebP, HEIC, HEIF, MP4, MOV, AVI, and WebM files. The
handleFiles()function (line 514 insailor-board/index.html) enforces a hard cap of 24 files per submission:Array.from(files).slice(0, 24).forEach(uploadFile). - Presigned URL fetch: JavaScript calls the Lambda endpoint
GET /api/g/{event_id}/presign?count=Nto retrieve presigned POST URLs for each file. - S3 direct upload: Browser sends multipart form data directly to S3 using the presigned URL. This keeps the Lambda/API Gateway request/response size bounded and offloads the bandwidth to S3.
- Metadata storage: After successful S3 upload, the client POSTs photo metadata (filename, upload timestamp, guest identifier) back to the Lambda at
POST /api/g/{event_id}/photos, which writes to DynamoDB.
Spam Prevention: Event Code Gate
The page includes a code input field (#g-code, line 354) and readCode() function (line 424). The logic:
- If the guest enters the event code, uploads are marked as
approved: trueand published immediately to the gallery. - If no code is entered, uploads go into a
moderation: pendingstate. The Lambda triggers an SNS notification (or manual review email) to the event organizer. - The page displays copy: "The code keeps strangers from posting to your charter page."
This pattern prevents spammers from discovering the URL and flooding the gallery with off-topic content, while still allowing unvetted guests to contribute (with review).
Real-Time Gallery and Instagram Integration
The page renders a grid of approved photos from the Lambda endpoint GET /api/g/{event_id}/photos, which returns a JSON payload containing:
- Approved guest photos (with thumbnail URLs for fast load)
- Instagram posts matching the event-specific hashtags (e.g.,
#jada,#queenofsandiego) from the same day
The IG feed is populated by the shipcaptaincrew Lambda's IG hashtag fetcher (integrated into the photo handler). The frontend renders both streams into #ig-grid (lines 452–459 in sailor-board/index.html) with a unified masonry layout.
This was a deliberate choice: instead of a separate IG widget, we unified the feed so guests see a mixed gallery of their own uploads plus organic social mentions—reinforcing the community narrative.
Infrastructure and Deployment
Lambda Packaging and Deployment
The Lambda function was built and deployed using a zip bundle containing:
- Core handler:
lambda_function.py(shipcaptaincrew module) - Dependencies: boto3 (AWS SDK), Pillow (image processing for thumbnail generation), requests (IG API client)
- Bundled library: py_vapid (for Web Push Notifications, used in other features)
Deployment workflow:
# Build zip with dependencies
zip -r lambda-deploy.zip lambda_function.py lib/
# Deploy to AWS Lambda
aws lambda update-function-code \
--function-name shipcaptaincrew-prod \
--zip-file fileb://lambda-deploy.zip \
--region us-west-2
The function was tested via smoke tests against the live endpoint before and after each deployment to verify presign, upload, metadata, and IG fetch still worked.
S3 CORS Configuration
To allow browser-based presigned uploads from queenofsandiego.com, we updated the S3 bucket CORS policy to allow:
- Origins:
https://queenofsandiego.com,https://*.queenofsandiego.com, staging origins - Methods: POST, PUT, GET, HEAD
- Headers:
*(to allow multipart form headers) - Max age: 3600 seconds
This was necessary because the presigned URL POST originates from the browser, and S3 enforces CORS preflight checks. Without it, uploads would fail with cryptic CORS errors.
Thumbnail Generation and Backfill
Photos uploaded to the gallery need fast-loading thumbnails for the masonry grid. We wrote a backfill script at /tmp/backfill-thumbs.py that:
- Scans the S3 bucket for original photos without corresponding thumbnails