Building the Sailor Board: Real-time Event Photo Curation with Lambda, S3, and Instagram Integration
What Was Done
We implemented a complete photo upload and curation system for event guest pages, centered on the "Sailor Board"—a per-event gallery that aggregates guest uploads and same-day Instagram posts matching event hashtags. The system launched with Keely's afternoon charter (2026-05-24) and is now production-ready for all future events.
The build includes:
- Guest upload UI with multi-file selection (up to 24 at once) and event-code gating to prevent spam
- Lambda-driven presigned S3 uploads with HEIC/HEIF/video support
- Automatic thumbnail generation and metadata extraction
- Same-day Instagram hashtag search and inline gallery rendering
- Moderation workflow with email notifications and one-click approval links
- Deep-link "Book a Sail" modal trigger from guest pages
Technical Architecture
Guest Upload Flow
The upload interface lives in /g/{event_id} guest pages. File handling is managed in the HTML form (around line 514-520 in the guest page template):
handleFiles() {
Array.from(files).slice(0, 24).forEach(uploadFile)
}
// Accepted MIME types: image/jpeg, image/png, image/webp, image/heic, image/heif, video/mp4, video/quicktime, video/x-msvideo, video/webm
The upload gate is enforced client-side and server-side:
- No code: Files enter a review queue (moderation required before publication)
- With event code: Files publish immediately to the gallery
The event code is passed in the `#g-code` input field (line 354) and validated by the Lambda presign endpoint.
Lambda Backend: shipcaptaincrew/lambda_function.py
The core upload handler is split into two main routes:
POST /presign— Returns AWS SigV4 presigned URLs for direct S3 uploadGET /api/g/{event_id}/photos— Serves the combined guest uploads + Instagram feed for rendering
Presign Endpoint Logic:
- Accepts event ID, file name, MIME type, and optional event code
- Validates the code against DynamoDB event record (table:
events, key:event_id) - If code matches, sets S3 object metadata
moderation_status=approved - If no code or invalid code, sets
moderation_status=pending - Returns 15-minute-expiry presigned PUT URL pointing to S3 bucket
qos-photos-produnder key pattern:events/{event_id}/{uuid}.{ext}
Photos API Endpoint:
- Queries DynamoDB table
event_photosfor all approved photos matching the event ID - Fetches Instagram metadata from the `instagram` field in the event record (populated by a separate IG fetcher Lambda)
- Merges both sources and returns as JSON with
photosandinstagramarrays - Each photo record includes: upload timestamp, uploader name/email, S3 key, thumbnail URL, original dimensions
Thumbnail Generation: Pillow Backfill
Initial photo uploads required retroactive thumbnail generation. We ran a backfill script (/tmp/backfill-thumbs.py) to process existing uploads:
#!/usr/bin/env python3
# Iterates event_photos DDB table
# For each approved photo in S3:
# - Download original
# - Generate 300x300 and 800x600 thumbnails via Pillow
# - Upload thumbs to qos-photos-prod under /thumbs/{uuid}_300.webp and /thumbs/{uuid}_800.webp
# - Update DDB record with thumb_url_small and thumb_url_large
Lambda runtime: Python 3.11 with Pillow==10.0.0 and boto3 (AWS SDK). Webp encoding is used for all thumbnails to reduce bandwidth without quality loss.
Instagram Integration
A separate scheduled Lambda (invoked ~4 hours before each event start time) queries Instagram's Graph API for posts matching hashtags #jada and #queenofsandiego from the same calendar day:
- Filters results to UTC timestamps within the 24-hour window
- Extracts media_type, caption, media_url, permalink, and timestamp
- Stores as a JSON array in the event record's
instagramfield (DynamoDB) - Keyed by event_id for fast lookup during the photos API call
The guest page renders this as a horizontal scrolling grid (lines 452–459 in the HTML) with attribution links back to Instagram.
Moderation Workflow
Photos uploaded without an event code trigger an SNS notification to moderators. The email (built in Lambda around line 2030–2070) includes:
- Uploader name and email
- Event details (date, charter name)
- Thumbnail preview
- One-click approve link:
https://queenofsandiego.com/admin/approve/{photo_id}?token={jwt} - Text: "Your photo is live. Welcome to the sailor board." (once approved)
The approve endpoint updates the DDB record moderation_status=approved, and the photo appears in the guest page gallery on next refresh.
Infrastructure & Deployment
S3 Buckets
qos-photos-prod— Photo uploads and thumbnails (public read, private write)queenofsandiego.com— Static site assets including guest page HTML templates
CORS Configuration on qos-photos-prod:
AllowedOrigins: ["https://queenofsandiego.com", "https://staging.queenofsandiego.com"]
AllowedMethods: ["GET", "PUT", "POST"]
AllowedHeaders: ["*"]
MaxAgeSeconds: 3600
This allows the browser to make preflight requests and presigned PUT uploads from guest pages without CORS errors.
Lambda Function Deployment
The shipcaptaincrew function is deployed to AWS Lambda with the following resource spec:
- Runtime: Python 3.11
- Memory: 512 MB