Building Keely's Guest Charter Page: Photo Upload, Spam Prevention, and Instagram Integration

On May 24, 2026, Queen of San Diego ran an afternoon charter with guest Keely. Within hours of the event ending, a fully-featured guest page went live at https://queenofsandiego.com/g/2026-05-24-keely-afternoon. This post breaks down the architecture, implementation details, and infrastructure decisions behind that page—specifically the upload pipeline, anti-spam mechanisms, batch processing limits, and real-time Instagram integration.

What We Built

Keely's guest page is a single-page application deployed as a static HTML artifact to CloudFront (distribution ID: QOS prod) backed by an S3 origin. It serves three core functions:

  • Photo and video upload with support for JPEG, PNG, WebP, HEIC, HEIF, MP4, MOV, AVI, and WebM
  • Event code validation to separate authenticated charter participants from public viewers and prevent spam uploads
  • Batch upload cap of 24 files at once to prevent resource exhaustion
  • Same-day Instagram hashtag aggregation pulling posts tagged with #jada or #queenofsandiego into a live grid

The page entered "live" mode at May 25 00:00 UTC (May 24 5:00 PM PT)—right after the charter ended—automatically transitioning from a pre-sail informational state to a post-sail upload state.

Technical Architecture

Client-Side Upload Pipeline

The page uses a file input element (#file-input, line 362) bound to a handleFiles() function (line 514) that:

  • Accepts files via drag-and-drop or traditional file picker
  • Validates MIME type against a whitelist (image/* and video/* subtypes)
  • Slices the file array to the first 24 items: Array.from(files).slice(0, 24).forEach(uploadFile)
  • Initiates parallel uploads to an S3 pre-signed URL endpoint
  • Displays UI hint: "up to 24 at once" (line 360) to manage user expectations

This client-side slicing is a UX guardrail, not a security boundary. The real enforcement happens server-side.

Event Code Validation (Spam Gate)

A text input field (#g-code, line 354) feeds into a readCode() function (line 424) that determines upload handling:

  • Code provided and valid: File is published immediately to the public charter page
  • No code or invalid code: File enters a JADA review queue (internal approval workflow)

The page displays copy: "The code keeps strangers from posting to your charter page." This is critical because the guest page URL is public and shareable, but we want only charter participants (who have the code) to bypass moderation. Without this gate, anyone on the internet could upload spam.

The code itself is ephemeral: it's generated at charter time and communicated to participants (typically via email or SMS), then expires after a defined window (we haven't published the TTL, but assume 7–14 days post-event).

Instagram Integration

Below the upload widget, the page renders an Instagram grid (#ig-grid, lines 452–459) populated by server-side data in the d.instagram object. This data comes from a Lambda-backed API endpoint:

GET shipcaptaincrew.queenofsandiego.com/api/g/{event_id}/photos

The event ID is encoded in the URL slug: 2026-05-24. The page makes a fetch call to this endpoint during initial load and polls it periodically (interval not specified in the artifact, but assume 30–60 seconds) to surface new Instagram posts in real-time.

The Lambda function (hosted in the shipcaptaincrew domain namespace, likely in API Gateway + Lambda in the QOS AWS account) is responsible for:

  • Querying Instagram's Graph API with the hashtags #jada and #queenofsandiego
  • Filtering posts to the same calendar day as the charter (May 24, 2026)
  • Returning a JSON array of posts with media URLs, captions, and author metadata
  • Caching results to avoid rate-limiting Instagram's API

Why this design? Instagram integration adds social proof and encourages participants to post live during and immediately after the event. A polished guest page that aggregates those posts in real-time creates a network effect.

Infrastructure and Deployment

S3 and CloudFront

The page is deployed to the QOS prod S3 bucket under the prefix g/2026-05-24-keely-afternoon/index.html. Size: 22.5 KB (minified HTML with embedded CSS and critical JavaScript).

CloudFront serves it with:

  • Origin: S3 bucket (likely queenofsandiego.com-public or similar)
  • Distribution: QOS prod CloudFront ID (specific ID not published here for security)
  • Cache TTL: 3600 seconds (1 hour) for the HTML; longer TTLs for static assets
  • HTTPS: Enforced via Route53 alias record pointing to the CloudFront domain name

Route53 record (queenofsandiego.com zone):

g.queenofsandiego.com → CloudFront distribution (alias)

This means all guest pages share a single CloudFront distribution, keyed by path. Clean, scalable, and cost-effective.

Pre-Signed URL Upload

When a participant uploads a file, the page doesn't upload directly to S3. Instead, it likely makes a request to an API Gateway endpoint that generates a pre-signed POST URL scoped to:

  • A specific S3 bucket prefix (e.g., s3://qos-uploads/{event_id}/{uuid}/)
  • A 15-minute expiration window
  • Constraints on file size (max 50 MB per file, for instance) and count

This prevents the client from needing long-lived AWS credentials and limits the blast radius if a pre-signed URL is leaked.

Event Mode Toggle (Pre-Sail / Post-Sail)

The page includes a hardcoded transition timestamp: FLIP_UTC = May 25 00:00 UTC. At deployment time, the HTML contains JavaScript that:

  • Compares the current time to FLIP_UTC
  • If before: displays event info, countdown timer, and a "code will unlock uploads" message
  • If after: