```html

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.instagram response 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
  • 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

  1. Pre-sail mode (before event time): Guest page shows event details, countdown, code input field for charter participants. Instagram grid is hidden.
  2. 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.
  3. Upload endpoint: POST /api/g/{event_id}/upload receives multipart data → Lambda validates event code → stores file to S3 → writes metadata to DynamoDB → responds with photo ID and moderation status.
  4. Photos endpoint: GET /api/g/{event_id}/photos queries 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=true immediately visible in gallery
  • No code provided: Photo enters moderation queue (approved=false), organizer receives email with approve/reject links, page shows pending_review state 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}/photos includes instagram array in JSON response
  • Hashtag configuration: #jada and #queenofsandiego are hardcoded search terms in lambda_function.py
  • Date filter: Only posts from event date (derived from event_id format YYYY-MM-DD-{time}) are included
  • Client-side rendering: Frontend iterates d.instagram array (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:

  1. Validated syntax locally: python -m py_compile lambda_function.py
  2. Bundled third-party dependencies (notably py_vapid for web push, if applicable) into deployment package
  3. Built zip artifact: zip -r deploy.zip lambda_function.py lib/ config/
  4. Verified zip contents: unzip -l deploy.zip | grep -E '(lambda_function|shipcaptaincrew)'
  5. Deployed to AWS: aws lambda update-function-code --function-name shipcaptaincrew --zip-file fileb://deploy.zip (timing: ~5–10 sec for code swap)
  6. 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