Overview

1

Photo Ingestion

Staff uploads DSLR photos to S3 tagged-photo inbox, keyed by camp name

2

Lambda Processing

Rekognition face detection → camper matching → API call to bridge-api → photo routed to processing bucket

3

Gallery Album Assignment

TaggedImage record created in DB · photo linked to the correct camp/week/day album

4

Manifest Generation & Publishing

Staff publishes album → manifest.json written to S3 media bucket → album status → Published

5

CampLife App Delivery

camplife-bridge-api serves gallery endpoints · Cloudinary CDN delivers photos at 4 sizes

1 · Photo Ingestion

Photos enter the system via the tagged-photo inbox S3 bucket (APP_SERVICES_AWS_S3_TAGGED_PHOTO_INBOX_BUCKET). An SQS queue (SQS_QUEUE_URL) receives S3 event notifications and fans them to the appropriate lambda.

Photo TypeInbox S3 Key
DSLR (staff camera)DSLR/{campName}/{filename}.jpg
Cabin portraitsCABIN-PHOTO/{campName}/{filename}.jpg
Family photosFAMILY-PHOTO/{campName}/{filename}.jpg

S3 object metadata headers carry additional context: cabin-group-id, family-group-id, camp-type.

2 · Lambda Processing

2.1 camplife-process-dslr-photos

File: camplife-bridge-lambda/camplife-process-dslr-photos/lambda_function.py

  1. Validates S3 key format (filetype/campName/filename)
  2. Downloads image to /tmp; extracts EXIF (DateTimeOriginal, Artist, XMP Rating/Label)
  3. Calls AWS Rekognition: detects all faces, searches collection for iCampProId matches
  4. Unmatched faces above quality threshold are indexed as UNKNOWN_{timestamp}
  5. Calls bridge-api POST /tagged-medias/process_dslr_info/ with match results
  6. On validTag=true: moves photo to PROCESSING_BUCKET/DSLR/ready/{campName}/
  7. On validTag=false: routes to error/duplicate/needslink sub-folders
  8. Embeds tag JSON back into EXIF UserComment field

Rekognition thresholds come from Secrets Manager: APP_SERVICES_AWS_REKOGNITION_CATALOGING_THRESHOLD and APP_SERVICES_AWS_REKOGNITION_PUBLISHING_THRESHOLD.

2.2 camplife-catalog-readied-photos

File: camplife-bridge-lambda/camplife-catalog-readied-photos/lambda_function.py

Triggered by photos arriving in PROCESSING_BUCKET/{filetype}/ready/.

  • Headshots: links UNKNOWN_* Rekognition faces to the correct iCampProId
  • Regular camper photos: calls bridge-api POST /tagged-medias/process_tag_info/ → creates TaggedImage DB record
  • Cabin/family: updates session group status

2.3 Other Lambdas

LambdaPurpose
camplife-process-tagged-photosPortal/mobile photo uploads (same flow as DSLR)
camplife-process-cabin-photosCabin portrait processing
camplife-gallery-photo-to-processingPromotes 5-star gallery photos to processing bucket
camplife-gallery-photo-to-gphotosArchives high-rated photos to Google Photos
camplife-gallery-photo-to-social-media-gphotosSocial media archive copy
camplife-cleanup-exif-dataStrips sensitive EXIF after processing

3 · Gallery Album Assignment

Gallery albums are created during season setup, one row per camp/week/day combination:

// GalleryAlbum entity
{
  title:     "Daily Photo Gallery",
  campName:  "Towers" | "Springs" | "Springs Pro" | …,
  eventYear: 2025,
  week:      "A" | "B" | "C" | …,
  day:       "Sun" | "Mon" | "Tues" | "Wed" | "Thurs" | "Fri" | "Sat",
  status:    InProgress → Scheduled → Ready → Published | Failed
}

S3 folder paths follow the formula:

{eventYear}/{locationName}/Week {seriesOrdinal}/Photos/{dayNumber}{day}/{albumTitle}
// Example: 2025/Towers/Week 1/Photos/1Sun/Daily Photo Gallery

The locationName is resolved by CampSessionService.resolveAppDisplayLocation — see the Springs vs Springs Pro section below for how this differs between those two camps.

Photos are linked to albums via the GalleryAlbumPhotos join table (galleryAlbumId + imageId). isCoverPhoto marks the album thumbnail.

4 · Manifest Generation & Publishing

InProgress Scheduled Ready Published Failed

When an album is published, media-gallery.service.ts scans the S3 folder and generates a manifest.json:

{
  "photos": [
    {
      "thumbnailURL":  "https://cdn.cloudinary.com/…?w=300",
      "fullscreenURL": "https://cdn.cloudinary.com/…?w=1280",
      "downloadURL":   "https://cdn.cloudinary.com/…?w=1920",
      "shareURL":      "https://…"
    }
  ]
}

The manifest is uploaded to {folderPath}/manifest.json. Publish is triggered via PUT /v1/gallery-albums/publish in camplife-bridge-api, accepting optional scheduledDate and scheduledTime.

5 · CampLife App Delivery

Both bridge-api and camplife-bridge-api share the same MySQL database. camplife-bridge-api is the read layer for the CampLife parent-facing mobile app.

Gallery endpoints (camplife-bridge-api)

File: camplife-bridge-api/server/src/web/rest/gallery-album.controller.ts

MethodEndpointPurpose
GET/v1/gallery-albums/List all albums (paginated)
GET/v1/gallery-albums/:idSingle album details
GET/v1/gallery-albums/:albumId/photosAll photos in an album
POST/v1/gallery-albums/listByStatusFilter by status, week, day, camp
PUT/v1/gallery-albums/publishPublish or schedule an album

Photo query

SELECT gap.*, image.*
FROM GalleryAlbumPhotos gap
LEFT JOIN Image image ON gap.imageId = image.id
WHERE gap.galleryAlbumId = :albumId
ORDER BY gap.isCoverPhoto DESC, image.dateTimeOriginal ASC

Photos are served as Cloudinary-transformed URLs at four sizes: thumbnail (300px) · fullscreen (1280px) · download (1920px) · original (4000px).

Springs vs Springs Pro: Stage-by-Stage

Springs and Springs Pro share the same physical location ("Pine Cove Springs"). resolveAppDisplayLocation for both YouthCamp-type sessions resolves to "Springs" after stripping the "Pine Cove " prefix — unless overridden.

An explicit override exists in gallery-folder-generator.ts:

// Lines 169–171  (standard session folder creation)
// Lines 776–778  (default album row creation)
if (campSession.camp.name === 'Springs Pro') {
    locationName = 'Springs Pro';
}

A parallel override in camplife-bridge-api/.../camper-publishing.service.ts ensures the CampLife app shows "Springs Pro" as the display location.

StageSpringsSprings Pro
Inbox S3 keyDSLR/Springs/{file}DSLR/Springs Pro/{file}
Processing bucket keyDSLR/ready/Springs/{file}DSLR/ready/Springs Pro/{file}
Album campName"Springs""Springs Pro"
Media bucket folder2025/Springs/Week N/Photos/…2025/Springs Pro/Week N/Photos/… (if provisioned)
Manifest location…/Springs/…/manifest.json…/Springs Pro/…/manifest.json
CampLife appDisplayLocationresolved from location entity"Springs Pro" (override)

Full Photo Journeys

Springs Photo (e.g., "Towers")

1. Inbox
DSLR/Towers/photo.jpg

2. Lambda (process-dslr-photos)
Rekognition: 2 matched, 1 UNKNOWN
POST /tagged-medias/process_dslr_info/
→ validTag=true
→ PROCESSING/DSLR/ready/Towers/photo.jpg

3. Lambda (catalog-readied-photos)
POST /tagged-medias/process_tag_info/
→ TaggedImage row created

4. Album assignment
campName="Towers", week="A", day="Sun"
GalleryAlbumPhotos row created

5. Staff publishes in Sidekick Portal
manifest.json → S3 media bucket
GalleryAlbum.status → Published

6. CampLife app
GET /v1/gallery-albums/{id}/photos
→ Cloudinary URLs at 300/1280/1920px

Springs Pro Photo

1. Inbox
DSLR/Springs Pro/photo.jpg

2. Lambda (process-dslr-photos)
Identical Rekognition flow
→ PROCESSING/DSLR/ready/Springs Pro/photo.jpg

3. Lambda (catalog-readied-photos)
Identical flow
→ TaggedImage row created

4. Album assignment
campName="Springs Pro"
S3 folder: 2025/Springs Pro/Week N/Photos/1Sun/
↑ Requires folder to be provisioned via
  createCloudFilesFoldersForStandardSessionAsync
  (override at gallery-folder-generator.ts:169
   already ensures locationName="Springs Pro")

5. Publish
Same flow, paths contain "Springs Pro"

6. CampLife app
appDisplayLocation = "Springs Pro"
(override in camper-publishing.service.ts)

Current Bug (as of 2026-05-14)

Springs Pro Final Destination folder never provisioned The createCloudFilesFoldersForStandardSessionAsync endpoint was never called for Springs Pro sessions. Photos tagged as Springs Pro are being routed to the Springs folder because resolveAppDisplayLocation returns "Springs" for both camps in the absence of the override. The image query pattern LIKE '%/Springs%' then matches both camp paths, causing Springs to show all photos while Springs Pro shows none.

See gallery-creator-springs-issue.html for the full diagnosis and fix options.