Gallery Photo Pipeline
Overview
Photo Ingestion
Staff uploads DSLR photos to S3 tagged-photo inbox, keyed by camp name
Lambda Processing
Rekognition face detection → camper matching → API call to bridge-api → photo routed to processing bucket
Gallery Album Assignment
TaggedImage record created in DB · photo linked to the correct camp/week/day album
Manifest Generation & Publishing
Staff publishes album → manifest.json written to S3 media bucket → album status → Published
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 Type | Inbox S3 Key |
|---|---|
| DSLR (staff camera) | DSLR/{campName}/{filename}.jpg |
| Cabin portraits | CABIN-PHOTO/{campName}/{filename}.jpg |
| Family photos | FAMILY-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
- Validates S3 key format (
filetype/campName/filename) - Downloads image to
/tmp; extracts EXIF (DateTimeOriginal,Artist, XMP Rating/Label) - Calls AWS Rekognition: detects all faces, searches collection for iCampProId matches
- Unmatched faces above quality threshold are indexed as
UNKNOWN_{timestamp} - Calls
bridge-api POST /tagged-medias/process_dslr_info/with match results - On
validTag=true: moves photo toPROCESSING_BUCKET/DSLR/ready/{campName}/ - On
validTag=false: routes to error/duplicate/needslink sub-folders - 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/→ createsTaggedImageDB record - Cabin/family: updates session group status
2.3 Other Lambdas
| Lambda | Purpose |
|---|---|
camplife-process-tagged-photos | Portal/mobile photo uploads (same flow as DSLR) |
camplife-process-cabin-photos | Cabin portrait processing |
camplife-gallery-photo-to-processing | Promotes 5-star gallery photos to processing bucket |
camplife-gallery-photo-to-gphotos | Archives high-rated photos to Google Photos |
camplife-gallery-photo-to-social-media-gphotos | Social media archive copy |
camplife-cleanup-exif-data | Strips 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
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
| Method | Endpoint | Purpose |
|---|---|---|
| GET | /v1/gallery-albums/ | List all albums (paginated) |
| GET | /v1/gallery-albums/:id | Single album details |
| GET | /v1/gallery-albums/:albumId/photos | All photos in an album |
| POST | /v1/gallery-albums/listByStatus | Filter by status, week, day, camp |
| PUT | /v1/gallery-albums/publish | Publish 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.
| Stage | Springs | Springs Pro |
|---|---|---|
| Inbox S3 key | DSLR/Springs/{file} | DSLR/Springs Pro/{file} |
| Processing bucket key | DSLR/ready/Springs/{file} | DSLR/ready/Springs Pro/{file} |
Album campName | "Springs" | "Springs Pro" |
| Media bucket folder | 2025/Springs/Week N/Photos/… | 2025/Springs Pro/Week N/Photos/… (if provisioned) |
| Manifest location | …/Springs/…/manifest.json | …/Springs Pro/…/manifest.json |
CampLife appDisplayLocation | resolved 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)
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.