Building the Sailor Board: Guest Photo Galleries, Real-Time Instagram Integration, and Event-Scoped Upload Gates
What Was Done
Over this session, we completed the implementation of per-event photo galleries (the "Sailor Board") for charter guests, deployed the upload infrastructure to production, wired up real-time Instagram hashtag search for same-day event posts, and fixed critical UX bugs in the booking flow. The work spanned four layers: Lambda function handlers, S3 CORS policy updates, CloudFront caching invalidation, and frontend modal behavior.
The Keely afternoon charter (2026-05-24) served as the live test case. Her guest page is now live at https://queenofsandiego.com/g/2026-05-24-keely-afternoon and accepts photo/video uploads from guests who provide the event code or route through moderation.
Technical Architecture
Photo Upload Pipeline
The upload flow is event-scoped and implements a two-path gate:
- Code path (instant publish): Guest enters the event code → Lambda validates against DynamoDB event table → presigned POST URL issued → S3 direct upload → DynamoDB photo record created immediately → appears in gallery
- No-code path (review queue): Guest uploads without code → photo stored in S3 staging prefix → DynamoDB record marked
status: 'pending_moderation'→ email notification sent to charter captain → captain approves/rejects via email link → photo moves to public prefix or is deleted
Code validation happens in shipcaptaincrew/lambda_function.py, specifically in the POST /api/g/{event_id}/presign handler (lines 1847–1920). The handler:
- Extracts
event_idandcodefrom the request - Queries DynamoDB
eventstable for matching event + code pair - Returns a presigned POST URL good for 15 minutes to the appropriate S3 prefix (
s3://qos-guest-photos/{event_id}/public/ors3://qos-guest-photos/{event_id}/pending/) - Frontend captures the response and uses it to upload directly to S3, bypassing Lambda
This design avoids Lambda payload limits and reduces cold-start latency for the upload itself. S3 event notifications (configured via Lambda triggers) then fire the on_photo_upload handler to generate thumbnails and index the photo in DynamoDB.
S3 CORS and Cross-Origin Policy
Guest pages are served from queenofsandiego.com, but presigned POST uploads target the qos-guest-photos bucket. We initially hit CORS preflight failures because the bucket's CORS configuration did not list https://queenofsandiego.com as an allowed origin.
Fix applied via AWS CLI:
aws s3api put-bucket-cors \
--bucket qos-guest-photos \
--cors-configuration file://cors-config.json
The cors-config.json includes:
{
"CORSRules": [
{
"AllowedOrigins": ["https://queenofsandiego.com", "https://staging.queenofsandiego.com"],
"AllowedMethods": ["POST", "PUT", "GET"],
"AllowedHeaders": ["*"],
"MaxAgeSeconds": 3000
}
]
}
This unblocks preflight OPTIONS requests from the browser and allows POST/PUT operations from guest pages.
Instagram Real-Time Hashtag Search
The same /api/g/{event_id}/photos endpoint (GET) that returns uploaded guest photos also includes a server-side Instagram feed fetch. The handler queries the Instagram Graph API for posts tagged with #jada or #queenofsandiego posted on the same day as the charter event.
The logic lives in lambda_function.py, approximately lines 1680–1750:
- Parse event start/end times from the event record
- Build an IG API query with hashtag IDs and date range filters
- Fetch media nodes and metadata (caption, image URL, timestamp)
- Filter for the matching date window (event day)
- Return as
d.instagramarray in the JSON response
The frontend (sailor-board/index.html, lines 452–459) renders the IG posts into a #ig-grid container alongside the guest photo grid. No additional client-side IG API call is needed; the server bundles everything into a single response.
Thumbnail Generation and Backfill
Original photos can be 4–8 MB. To improve gallery load times, we generate 300×200 thumbnails and store them in the same S3 prefix with a _thumb suffix. The Lambda trigger (on_photo_upload) runs after each S3 PUT and uses Pillow to resize images and ffmpeg to extract video frames.
For Keely's event, we ran a backfill script (/tmp/backfill-thumbs.py) to retroactively generate thumbnails for any photos uploaded before the thumbnail handler was in place. This ensures the gallery renders quickly even for events with hundreds of photos.
Frontend: Embedding the Booking Modal
The "Book a Sail" button on the homepage initially linked back to the homepage, breaking the UX. We extracted the Stripe checkout modal from the original booking-widget.js and embedded it as a script bundle in the main homepage template.
Changes made to index.html:
- Moved the modal trigger handler from a separate script to inline JavaScript in the
<head> - Added a data attribute to the "Book a Sail" button:
data-open-booking-modal="true" - On
DOMContentLoaded, attach a click listener that callsopenBookingModal()directly instead of navigating - The modal fetches Stripe session config from
POST /api/checkouton the Lambda backend and opens the Stripe Checkout iframe in-page
This eliminates the round-trip back to homepage and keeps users in the conversion flow.
Infrastructure and Deployment
Lambda Function Updates
The shipcaptaincrew Lambda function (Node.js 18, 512 MB memory) handles:
POST /api/g/{event_id}/presign— presigned URL issuanceGET /api/g/{event_id}/photos— gallery data + IG feedPOST /api/checkout— Stripe session creationPOST /api/g/{event_id}/moderate— captain approval/rejection
Deployment pipeline: