Building the Sailor Board: Guest Photo Gallery & Real-Time Instagram Integration for Charter Events
Overview
During this session, we completed the implementation and deployment of the Sailor Board — a per-charter guest photo gallery with real-time Instagram hashtag integration, anti-spam gating via event codes, and batch upload capability. The feature went live on Keely's charter event page (https://queenofsandiego.com/g/2026-05-24-keely-afternoon) on May 25, 2026, and we've now formalized the architecture, fixed critical booking modal behavior, and documented the full stack for future events.
What Was Built
- Guest upload flow: Authenticated file handling with 24-file-at-once batch support, media validation (JPEG, PNG, WebP, HEIC, HEIF, MP4, MOV, AVI, WebM), and dual-path publishing: instant via event code, or moderation queue for anonymous uploads.
-
Per-charter photo gallery: CloudFront-cached HTML page served from S3 at
/g/{event_id}/index.htmlwith client-side rendering of photos, videos, and synchronized Instagram feed. -
Instagram real-time pull: Lambda-driven hashtag search (
#jada,#queenofsandiego) filtered to same-day posts, served via API endpointGET /api/g/{event_id}/photos. - Anti-spam gating: Per-charter event codes distributed to legitimate guests; code holders skip moderation. Non-code uploads queued for review by charter operators.
- Booking modal deep-link fix: "Book a Sail" CTA now triggers immediate modal open instead of homepage navigation.
Technical Architecture
File Structure & Deployment Targets
/Users/cb/Documents/repos/sites/queenofsandiego.com/
├── sailor-board/index.html # Per-charter guest page template
├── index.html # Homepage (updated nav + Book CTA)
├── booking-widget.js # Extracted modal trigger + Stripe integration
└── tools/shipcaptaincrew/
└── lambda_function.py # AWS Lambda handler for /api/g/* endpoints
The sailor board is not a centralized page but a template-driven pattern: for each charter event, we generate an S3 object at s3://queenofsandiego-public/g/{YYYY-MM-DD-event-slug}/index.html. The Lambda function at shipcaptaincrew.queenofsandiego.com serves the API layer.
Data Flow: Uploads → Storage → Gallery Render
-
Client upload: Guest selects up to 24 files via
<input id="file-input" multiple>in the sailor-board HTML. JavaScript callshandleFiles()(line 514 in sailor-board/index.html), which slices to first 24 and triggers presigned POST to S3. -
S3 presign flow:
GET /api/g/{event_id}/presignLambda endpoint returns time-limited presigned POST URL. CORS headers allowOrigin: https://queenofsandiego.comandOrigin: https://staging.queenofsandiego.com. -
DynamoDB metadata: After successful S3 PUT, a Lambda trigger (or client-side callback) writes upload metadata to DynamoDB table
queenofsandiego-uploadswith partition key{event_id}#{timestamp}. Includes: uploader identity, code verification status, moderation flag. -
Gallery render:
GET /api/g/{event_id}/photosqueries DynamoDB, retrieves S3 object metadata (size, format), and joins approved uploads with Instagram feed data. Returns JSON array to client for client-side grid render. -
Instagram sync: Lambda periodically or on-demand fetches posts tagged
#jadaor#queenofsandiegofrom the previous 24 hours relative toevent_iddate. Instagram Graph API credentials stored in AWS Secrets Manager; queries filtered bytimestamp >= event_dateANDtimestamp < event_date + 1 day.
Caching & CDN Strategy
Per-charter pages are deployed to S3 and served through CloudFront distribution d3m5f7k9x2n1q.cloudfront.net (staging) and production alias queenofsandiego.com (via Route53 weighted routing). Cache headers:
Cache-Control: max-age=3600, publicfor sailor-board HTML (1 hour, to pick up moderation changes).Cache-Control: max-age=86400, publicfor photo thumbnails (24 hours, assumes no deletion).Cache-Control: no-cache, no-storefor/api/g/*(always fresh from Lambda).
CloudFront cache invalidation is triggered post-deployment via aws cloudfront create-invalidation --distribution-id {DIST_ID} --paths "/g/*" "/api/g/*".
Lambda & Anti-Spam Implementation
The shipcaptaincrew Lambda function (file: tools/shipcaptaincrew/lambda_function.py) contains three critical handlers:
1. Event Code Verification (readCode() — client-side; server validates)
When a guest enters their event code in the #g-code input, JavaScript calls the presign endpoint with header X-Event-Code: {code}. The Lambda handler compares the code against Event table in DynamoDB:
def verify_event_code(event_id, supplied_code):
# Query DynamoDB Event table, partition key = event_id
event_record = dynamodb.get_item(
TableName='queenofsandiego-events',
Key={'event_id': {'S': event_id}}
)
stored_code = event_record['Item']['guest_code']['S']
return supplied_code == stored_code
If code matches, uploads bypass moderation queue. If no code or mismatch, upload is marked moderation_pending: true in DynamoDB, and a notification is sent to the charter operator's email.
2. Presign Endpoint (POST /api/g/{event_id}/presign)
def lambda_handler(event, context):
event_id = event['pathParameters']['event_id']
code = event['headers'].get('X