```html

Building the Sailor Board: Guest Photo Galleries, Real-Time Instagram Integration, and the Anti-Spam Event Code Pattern

What Was Done

Over this session, we completed the full feature stack for Queen of San Diego's per-charter guest photo galleries—nicknamed "Sailor Boards." The implementation includes:

  • Per-event guest pages at /g/{event_date}-{guest_name} with photo/video upload capability
  • Event code gating to prevent spam uploads and control moderation flow
  • 24-at-once batch upload with JPEG/PNG/WebP/HEIC/HEIF + MP4/MOV/AVI/WebM support
  • Same-day Instagram hashtag (#jada, #queenofsandiego) auto-discovery and rendering
  • Lambda-driven moderation queue with email routing
  • Staging/prod deployment pipeline with CloudFront cache invalidation

Technical Details: The Guest Page Architecture

File Structure:

Guest pages are deployed to S3 at s3://queenofsandiego-public/g/{event_id}/index.html. During this session, we created the template in /Users/cb/Documents/repos/sites/queenofsandiego.com/sailor-board/index.html and validated it against the live Keely event page (2026-05-24-keely-afternoon), which was already in production.

Upload Flow (Client-Side):

The page includes a file input bound to #file-input (line 362) that accepts the media types listed above. When users select files, the handleFiles() function (line 514) triggers:

Array.from(files).slice(0, 24).forEach(uploadFile)

This caps batch uploads at 24 items per submission. Each file is sent via a presigned POST to the charter's S3 prefix:

GET /api/presign?event_id={event_id}&guest_code={code}&filename={filename}

The presigned URL is generated by the shipcaptaincrew Lambda and includes the guest's event code in the object metadata, allowing the moderation backend to validate ownership.

Event Code Gating (Spam Prevention):

This is the critical anti-spam pattern. A #g-code input (line 354) accepts a short alphanumeric code provided to guests at booking time. The readCode() function (line 424) validates it:

  • Code provided: Upload is marked as pre-approved. It bypasses the moderation queue and goes live immediately to the gallery.
  • No code: Upload enters a JADA review queue. A moderator receives an email notification and can approve/reject before the photo appears on the page.

The copy on the page reads: "The code keeps strangers from posting to your charter page." This is the key UX pattern—guests with the secret code get instant gratification; random visitors who find the URL must wait for manual approval.

Instagram Integration: Same-Day Hashtag Discovery

The page renders an Instagram grid (lines 452–459) populated by the backend. When the page loads, it calls:

GET /api/g/{event_id}/photos

This endpoint (in tools/shipcaptaincrew/lambda_function.py) returns a JSON object with two properties:

  • photos — approved uploads from S3 for this charter
  • instagram — posts from Instagram matching #jada or #queenofsandiego, filtered to the event date (e.g., May 24, 2026)

The IG feed is fetched server-side (likely via the Instagram Graph API or a pre-cached feed) and merged with guest uploads. This lets guests and moderators see real-time social content without exposing API credentials to the client.

Infrastructure: Deployment and Caching

S3 Deployment:

Guest pages are deployed to s3://queenofsandiego-public/g/{event_id}/index.html. The bucket is configured with:

  • CORS policy allowing queenofsandiego.com and *.queenofsandiego.com origins for presigned POST requests
  • Versioning enabled for audit trail (important for event-sensitive content)
  • Server-side encryption (AES-256)

CloudFront Distribution:

Staging and production distributions serve the S3 bucket. During this session, we:

  1. Deployed the sailor-board page to the staging distribution
  2. Invalidated the CloudFront cache via aws cloudfront create-invalidation --distribution-id {STAGING_DIST_ID} --paths "/g/*"
  3. Smoke-tested the staging URL to verify upload and IG rendering worked end-to-end
  4. Then deployed to prod with the same invalidation pattern

Cache behavior is set to:

  • Static HTML: 60 seconds (short TTL because uploads and IG posts change frequently within the event window)
  • API endpoints: No caching (CloudFront bypassed for /api/*)
  • Media: 1 year (once a photo is uploaded and approved, it doesn't change)

Lambda: Photo Handling and Moderation

File: tools/shipcaptaincrew/lambda_function.py

Key functions:

  • presign_upload() — generates presigned S3 POST URLs, validates event code, returns bucket/key prefix
  • get_event_photos() — lists approved photos from DynamoDB, queries IG API, merges results
  • handle_upload_notification() — triggered by S3 event; checks event code, either auto-publishes (code valid) or creates moderation task (no code)
  • approve_photo() — moderator action; moves photo from staging table to live gallery table, triggers email to guest

The moderation table (DynamoDB) uses:

PK: event_id#upload_id
SK: created_timestamp
Attributes: guest_code, filename, bucket_key, status (pending|approved|rejected), moderator_notes

This allows O(1) lookup by event and fast range queries by timestamp for a moderator dashboard.

Key Design Decisions

Why Event Codes Instead of Email Verification?

At a charter, guests are already with you in person or via a booking confirmation. An email verification loop adds friction and latency. A short code (6–8 chars) is shared verbally during pre-sail briefing or in the Stripe order confirmation. It's memorable, revocable per-event, and requires zero extra infrastructure.

Why Merge Instagram