Building the Sailor Board: Implementing Per-Event Photo Galleries with Lambda, S3, and Instagram Integration
Overview: What We Built
During this session, we implemented and deployed a complete photo-sharing feature for charter event guest pages on Queen of San Diego. The "Sailor Board" is a per-event photo gallery that allows guests to upload up to 24 photos/videos simultaneously, with optional Instagram integration to pull same-day posts matching event-specific hashtags. The feature includes spam prevention via event codes, asynchronous moderation workflows, and real-time image rendering—all deployed live for Keely's May 24, 2026 afternoon charter.
Technical Architecture
File Structure and Components
The implementation spans three core layers:
- Frontend Guest Page:
/sites/queenofsandiego.com/g/{event_id}/index.html- Dynamic page generated per-charter with event metadata baked into the DOM
- Client-side file input handling with
handleFiles()function (line 514) - Accepts JPEG, PNG, WebP, HEIC, HEIF for photos; MP4, MOV, AVI, WebM for video
- 24-file limit enforced client-side:
Array.from(files).slice(0, 24).forEach(uploadFile) - Instagram grid rendering at lines 452–459 pulls from
d.instagramresponse object
- Lambda Backend:
/tools/shipcaptaincrew/lambda_function.py- HTTP handler for
POST /api/g/{event_id}/upload— processes multipart file uploads - HTTP handler for
GET /api/g/{event_id}/photos— returns photo metadata and Instagram feed data - Event code validation logic (line 424:
readCode()) gates instant publication vs. moderation queue - Instagram hashtag fetcher pulls posts from configured hashtags (
#jada,#queenofsandiego) within 24-hour event window - Moderation workflow generates email notifications to event organizer with approval links
- HTTP handler for
- Storage: S3 bucket for per-event photo assets
- Bucket constant defined in Lambda for predictable path construction
- Objects stored under
s3://{bucket}/g/{event_id}/photos/{photo_id}.{ext} - Metadata (caption, uploader, timestamp, moderation status) stored in DynamoDB
Request Flow
- Pre-sail mode (before event time): Guest page shows event details, countdown, code input field for charter participants. Instagram grid is hidden.
- Post-sail mode (after FLIP_UTC = event end time): File upload UI becomes active. Guests can submit photos without code, which routes to moderation queue. Instagram feed populates with same-day hashtag matches.
- Upload endpoint:
POST /api/g/{event_id}/uploadreceives multipart data → Lambda validates event code → stores file to S3 → writes metadata to DynamoDB → responds with photo ID and moderation status. - Photos endpoint:
GET /api/g/{event_id}/photosqueries DynamoDB for approved photos + fetches Instagram feed data → returns JSON with both collections.
Key Implementation Details
Spam Prevention: Event Code Gate
The guest page includes an optional event code input (line 354: #g-code) with validation logic in Lambda's readCode() function (line 424). Here's the decision tree:
- Code provided + matches event code: Photo marked as
approved=trueimmediately visible in gallery - No code provided: Photo enters moderation queue (
approved=false), organizer receives email with approve/reject links, page showspending_reviewstate to uploader - UI messaging clarifies intent: *"The code keeps strangers from posting to your charter page."* (line 350)
This prevents spam while allowing post-event public submission—event participants use the code to bypass review, external followers submit to a reviewed queue.
24-Photo Batch Upload
Client-side slicing (line 515) limits file selection to 24 items, with UI hint at line 360: *"up to 24 at once"*. The rationale:
- Balances user convenience (one upload gesture) against Lambda timeout risk (~15-minute hard limit)
- 24 × ~3 MB average = ~72 MB total payload, well within Lambda's 6 GB ephemeral storage
- Parallel uploads via Promise.all() ensure reasonable wall-clock time (~30–60 sec for typical broadband)
Instagram Integration
The feature pulls Instagram posts matching event hashtags within the event date window. Implementation lives in Lambda's Instagram fetcher module:
- Endpoint:
GET /api/g/{event_id}/photosincludesinstagramarray in JSON response - Hashtag configuration:
#jadaand#queenofsandiegoare hardcoded search terms inlambda_function.py - Date filter: Only posts from event date (derived from
event_idformatYYYY-MM-DD-{time}) are included - Client-side rendering: Frontend iterates
d.instagramarray (lines 452–459) and builds grid HTML dynamically
Why this pattern? Server-side fetching keeps Instagram credentials secure and avoids client-side token exposure. Client-side rendering keeps the page responsive and avoids page regeneration delays.
Deployment and Infrastructure
Lambda Deployment Process
We followed this workflow to deploy code changes:
- Validated syntax locally:
python -m py_compile lambda_function.py - Bundled third-party dependencies (notably
py_vapidfor web push, if applicable) into deployment package - Built zip artifact:
zip -r deploy.zip lambda_function.py lib/ config/ - Verified zip contents:
unzip -l deploy.zip | grep -E '(lambda_function|shipcaptaincrew)' - Deployed to AWS:
aws lambda update-function-code --function-name shipcaptaincrew --zip-file fileb://deploy.zip(timing: ~5–10 sec for code swap) - Smoke tested via curl:
curl https://shipcaptaincrew.queenofsandiego.com/api/g/2026-05-24-keely-afternoon/photos
The deployment snapshot was captured from prod before local edits were applied, ensuring we could diff function-level changes and roll back if needed